From 889f29196149f87730c92c4ed0fc88e2d8660057 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Tue, 6 Dec 2022 16:10:56 -0800 Subject: [PATCH 01/22] Update notebooks to use latest codes with extra notebook dependencies Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- .../databricks_quickstart_nyc_taxi_demo.ipynb | 4 +- docs/samples/fraud_detection_demo.ipynb | 2509 ++++++++++++++++- docs/samples/nyc_taxi_demo.ipynb | 31 +- feathr_project/feathr/datasets/constants.py | 8 + feathr_project/setup.py | 1 + 5 files changed, 2423 insertions(+), 130 deletions(-) diff --git a/docs/samples/databricks/databricks_quickstart_nyc_taxi_demo.ipynb b/docs/samples/databricks/databricks_quickstart_nyc_taxi_demo.ipynb index bd259b5d8..6f5f9bfff 100644 --- a/docs/samples/databricks/databricks_quickstart_nyc_taxi_demo.ipynb +++ b/docs/samples/databricks/databricks_quickstart_nyc_taxi_demo.ipynb @@ -70,8 +70,8 @@ }, "outputs": [], "source": [ - "# Install feathr from the latest codes in the repo. You may use `pip install feathr` as well.\n", - "!pip install \"git+https://github.com/feathr-ai/feathr#subdirectory=feathr_project\"" + "# Install feathr from the latest codes in the repo. You may use `pip install feathr[notebook]` as well.\n", + "!pip install \"git+https://github.com/feathr-ai/feathr.git#subdirectory=feathr_project&egg=feathr[notebook]\"" ] }, { diff --git a/docs/samples/fraud_detection_demo.ipynb b/docs/samples/fraud_detection_demo.ipynb index 1e57604ae..fc5993d22 100644 --- a/docs/samples/fraud_detection_demo.ipynb +++ b/docs/samples/fraud_detection_demo.ipynb @@ -49,7 +49,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, @@ -58,14 +58,326 @@ "title": "" } }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting feathr[notebook]\n", + " Cloning https://github.com/feathr-ai/feathr.git to /tmp/pip-install-zsn6kl5o/feathr_36df02ae02da42a0a56397c6e9e058e7\n", + " Running command git clone --filter=blob:none --quiet https://github.com/feathr-ai/feathr.git /tmp/pip-install-zsn6kl5o/feathr_36df02ae02da42a0a56397c6e9e058e7\n", + " Resolved https://github.com/feathr-ai/feathr.git to commit 3ebaa49cf36dac90005fb2cbf5412d9103871e9b\n", + " Installing build dependencies ... \u001b[?25ldone\n", + "\u001b[?25h Getting requirements to build wheel ... \u001b[?25ldone\n", + "\u001b[?25h Preparing metadata (pyproject.toml) ... \u001b[?25ldone\n", + "\u001b[?25hCollecting py4j<=0.10.9.7\n", + " Using cached py4j-0.10.9.7-py2.py3-none-any.whl (200 kB)\n", + "Collecting Jinja2<=3.1.2\n", + " Using cached Jinja2-3.1.2-py3-none-any.whl (133 kB)\n", + "Collecting pyspark>=3.1.2\n", + " Downloading pyspark-3.3.1.tar.gz (281.4 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m281.4/281.4 MB\u001b[0m \u001b[31m7.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", + "\u001b[?25h Preparing metadata (setup.py) ... \u001b[?25ldone\n", + "\u001b[?25hCollecting confluent-kafka<=1.9.2\n", + " Using cached confluent_kafka-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.1 MB)\n", + "Collecting requests<=2.28.1\n", + " Using cached requests-2.28.1-py3-none-any.whl (62 kB)\n", + "Collecting azure-synapse-spark<=0.7.0\n", + " Using cached azure_synapse_spark-0.7.0-py2.py3-none-any.whl (32 kB)\n", + "Collecting databricks-cli<=0.17.3\n", + " Using cached databricks_cli-0.17.3-py3-none-any.whl\n", + "Collecting pyyaml<=6.0\n", + " Using cached PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (682 kB)\n", + "Collecting pyarrow<=9.0.0\n", + " Using cached pyarrow-9.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (35.3 MB)\n", + "Collecting pandas<=1.5.0\n", + " Using cached pandas-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (12.1 MB)\n", + "Collecting azure-storage-file-datalake<=12.5.0\n", + " Using cached azure_storage_file_datalake-12.5.0-py2.py3-none-any.whl (208 kB)\n", + "Collecting pandavro<=1.7.1\n", + " Using cached pandavro-1.7.1-py3-none-any.whl\n", + "Collecting tqdm<=4.64.1\n", + " Using cached tqdm-4.64.1-py2.py3-none-any.whl (78 kB)\n", + "Collecting redis<=4.4.0\n", + " Downloading redis-4.4.0-py3-none-any.whl (236 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m236.4/236.4 kB\u001b[0m \u001b[31m8.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting azure-identity>=1.8.0\n", + " Downloading azure_identity-1.12.0-py3-none-any.whl (135 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m135.5/135.5 kB\u001b[0m \u001b[31m11.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting azure-keyvault-secrets<=4.6.0\n", + " Using cached azure_keyvault_secrets-4.6.0-py3-none-any.whl (291 kB)\n", + "Collecting graphlib-backport<=1.0.3\n", + " Using cached graphlib_backport-1.0.3-py3-none-any.whl (5.1 kB)\n", + "Collecting click<=8.1.3\n", + " Using cached click-8.1.3-py3-none-any.whl (96 kB)\n", + "Collecting pyhocon<=0.3.59\n", + " Using cached pyhocon-0.3.59-py3-none-any.whl\n", + "Collecting msrest<=0.6.21\n", + " Using cached msrest-0.6.21-py2.py3-none-any.whl (85 kB)\n", + "Collecting azure-core<=1.22.1\n", + " Using cached azure_core-1.22.1-py3-none-any.whl (178 kB)\n", + "Collecting protobuf<=3.19.4,>=3.0.0\n", + " Downloading protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.1 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.1/1.1 MB\u001b[0m \u001b[31m13.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", + "\u001b[?25hCollecting loguru<=0.6.0\n", + " Using cached loguru-0.6.0-py3-none-any.whl (58 kB)\n", + "Collecting typing-extensions>=4.2.0\n", + " Downloading typing_extensions-4.4.0-py3-none-any.whl (26 kB)\n", + "Collecting deltalake>=0.6.2\n", + " Downloading deltalake-0.6.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (14.7 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m14.7/14.7 MB\u001b[0m \u001b[31m20.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", + "\u001b[?25hCollecting avro<=1.11.1\n", + " Using cached avro-1.11.1-py2.py3-none-any.whl\n", + "Collecting python-snappy<=0.6.1\n", + " Using cached python_snappy-0.6.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (55 kB)\n", + "Collecting pyapacheatlas<=0.14.0\n", + " Using cached pyapacheatlas-0.14.0-py3-none-any.whl (74 kB)\n", + "Collecting plotly\n", + " Downloading plotly-5.11.0-py2.py3-none-any.whl (15.3 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m15.3/15.3 MB\u001b[0m \u001b[31m20.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", + "\u001b[?25hCollecting jupyter==1.0.0\n", + " Using cached jupyter-1.0.0-py2.py3-none-any.whl (2.7 kB)\n", + "Collecting scrapbook<1.0.0,>=0.5.0\n", + " Using cached scrapbook-0.5.0-py3-none-any.whl (34 kB)\n", + "Collecting papermill<3,>=2.1.2\n", + " Using cached papermill-2.4.0-py3-none-any.whl (38 kB)\n", + "Collecting scikit-learn\n", + " Downloading scikit_learn-1.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (30.5 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m30.5/30.5 MB\u001b[0m \u001b[31m16.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", + "\u001b[?25hCollecting matplotlib==3.6.1\n", + " Using cached matplotlib-3.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.8 MB)\n", + "Collecting jupyter-console\n", + " Using cached jupyter_console-6.4.4-py3-none-any.whl (22 kB)\n", + "Collecting ipywidgets\n", + " Using cached ipywidgets-8.0.2-py3-none-any.whl (134 kB)\n", + "Requirement already satisfied: ipykernel in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from jupyter==1.0.0->feathr[notebook]) (6.17.1)\n", + "Collecting qtconsole\n", + " Downloading qtconsole-5.4.0-py3-none-any.whl (121 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m121.0/121.0 kB\u001b[0m \u001b[31m12.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting nbconvert\n", + " Downloading nbconvert-7.2.6-py3-none-any.whl (273 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m273.2/273.2 kB\u001b[0m \u001b[31m14.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting notebook\n", + " Downloading notebook-6.5.2-py3-none-any.whl (439 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m439.1/439.1 kB\u001b[0m \u001b[31m21.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting cycler>=0.10\n", + " Using cached cycler-0.11.0-py3-none-any.whl (6.4 kB)\n", + "Requirement already satisfied: packaging>=20.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from matplotlib==3.6.1->feathr[notebook]) (21.3)\n", + "Collecting fonttools>=4.22.0\n", + " Downloading fonttools-4.38.0-py3-none-any.whl (965 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m965.4/965.4 kB\u001b[0m \u001b[31m15.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: python-dateutil>=2.7 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from matplotlib==3.6.1->feathr[notebook]) (2.8.2)\n", + "Collecting pillow>=6.2.0\n", + " Downloading Pillow-9.3.0-cp310-cp310-manylinux_2_28_x86_64.whl (3.3 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m3.3/3.3 MB\u001b[0m \u001b[31m7.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0mm\n", + "\u001b[?25hCollecting kiwisolver>=1.0.1\n", + " Using cached kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.6 MB)\n", + "Requirement already satisfied: pyparsing>=2.2.1 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from matplotlib==3.6.1->feathr[notebook]) (3.0.9)\n", + "Collecting numpy>=1.19\n", + " Downloading numpy-1.23.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (17.1 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m17.1/17.1 MB\u001b[0m \u001b[31m12.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", + "\u001b[?25hCollecting contourpy>=1.0.1\n", + " Downloading contourpy-1.0.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (296 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m296.1/296.1 kB\u001b[0m \u001b[31m16.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: six>=1.11.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from azure-core<=1.22.1->feathr[notebook]) (1.16.0)\n", + "Collecting cryptography>=2.5\n", + " Downloading cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl (4.2 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m4.2/4.2 MB\u001b[0m \u001b[31m17.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", + "\u001b[?25hCollecting msal-extensions<2.0.0,>=0.3.0\n", + " Using cached msal_extensions-1.0.0-py2.py3-none-any.whl (19 kB)\n", + "Collecting msal<2.0.0,>=1.12.0\n", + " Downloading msal-1.20.0-py2.py3-none-any.whl (90 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m90.0/90.0 kB\u001b[0m \u001b[31m7.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting azure-common~=1.1\n", + " Using cached azure_common-1.1.28-py2.py3-none-any.whl (14 kB)\n", + "Collecting azure-storage-blob<13.0.0,>=12.9.0b1\n", + " Downloading azure_storage_blob-12.14.1-py3-none-any.whl (383 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m383.2/383.2 kB\u001b[0m \u001b[31m1.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m\n", + "\u001b[?25hCollecting oauthlib>=3.1.0\n", + " Downloading oauthlib-3.2.2-py3-none-any.whl (151 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m151.7/151.7 kB\u001b[0m \u001b[31m9.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting pyjwt>=1.7.0\n", + " Downloading PyJWT-2.6.0-py3-none-any.whl (20 kB)\n", + "Collecting tabulate>=0.7.7\n", + " Downloading tabulate-0.9.0-py3-none-any.whl (35 kB)\n", + "Collecting MarkupSafe>=2.0\n", + " Using cached MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (25 kB)\n", + "Collecting requests-oauthlib>=0.5.0\n", + " Using cached requests_oauthlib-1.3.1-py2.py3-none-any.whl (23 kB)\n", + "Collecting isodate>=0.6.0\n", + " Using cached isodate-0.6.1-py2.py3-none-any.whl (41 kB)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from msrest<=0.6.21->feathr[notebook]) (2022.9.24)\n", + "Collecting pytz>=2020.1\n", + " Downloading pytz-2022.6-py2.py3-none-any.whl (498 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m498.1/498.1 kB\u001b[0m \u001b[31m17.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting fastavro==1.5.1\n", + " Using cached fastavro-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.5 MB)\n", + "Collecting tenacity\n", + " Using cached tenacity-8.1.0-py3-none-any.whl (23 kB)\n", + "Requirement already satisfied: entrypoints in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from papermill<3,>=2.1.2->feathr[notebook]) (0.4)\n", + "Collecting nbformat>=5.1.2\n", + " Using cached nbformat-5.7.0-py3-none-any.whl (77 kB)\n", + "Collecting nbclient>=0.2.0\n", + " Downloading nbclient-0.7.2-py3-none-any.whl (71 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m72.0/72.0 kB\u001b[0m \u001b[31m4.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting ansiwrap\n", + " Using cached ansiwrap-0.8.4-py2.py3-none-any.whl (8.5 kB)\n", + "Collecting openpyxl>=3.0\n", + " Using cached openpyxl-3.0.10-py2.py3-none-any.whl (242 kB)\n", + "Collecting pyparsing>=2.2.1\n", + " Using cached pyparsing-2.4.7-py2.py3-none-any.whl (67 kB)\n", + "Collecting py4j<=0.10.9.7\n", + " Using cached py4j-0.10.9.5-py2.py3-none-any.whl (199 kB)\n", + "Collecting async-timeout>=4.0.2\n", + " Using cached async_timeout-4.0.2-py3-none-any.whl (5.8 kB)\n", + "Collecting charset-normalizer<3,>=2\n", + " Using cached charset_normalizer-2.1.1-py3-none-any.whl (39 kB)\n", + "Collecting urllib3<1.27,>=1.21.1\n", + " Downloading urllib3-1.26.13-py2.py3-none-any.whl (140 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m140.6/140.6 kB\u001b[0m \u001b[31m7.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting idna<4,>=2.5\n", + " Using cached idna-3.4-py3-none-any.whl (61 kB)\n", + "Requirement already satisfied: ipython in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (8.7.0)\n", + "Collecting jsonschema\n", + " Downloading jsonschema-4.17.3-py3-none-any.whl (90 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m90.4/90.4 kB\u001b[0m \u001b[31m8.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting threadpoolctl>=2.0.0\n", + " Downloading threadpoolctl-3.1.0-py3-none-any.whl (14 kB)\n", + "Collecting joblib>=1.0.0\n", + " Downloading joblib-1.2.0-py3-none-any.whl (297 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m298.0/298.0 kB\u001b[0m \u001b[31m4.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", + "\u001b[?25hCollecting scipy>=1.3.2\n", + " Downloading scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (33.7 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m33.7/33.7 MB\u001b[0m \u001b[31m16.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", + "\u001b[?25hCollecting azure-storage-blob<13.0.0,>=12.9.0b1\n", + " Downloading azure_storage_blob-12.14.0-py3-none-any.whl (383 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m383.2/383.2 kB\u001b[0m \u001b[31m14.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25h Using cached azure_storage_blob-12.14.0b2-py3-none-any.whl (383 kB)\n", + " Using cached azure_storage_blob-12.14.0b1-py3-none-any.whl (381 kB)\n", + " Using cached azure_storage_blob-12.13.1-py3-none-any.whl (377 kB)\n", + " Using cached azure_storage_blob-12.13.0-py3-none-any.whl (377 kB)\n", + " Using cached azure_storage_blob-12.13.0b1-py3-none-any.whl (377 kB)\n", + " Using cached azure_storage_blob-12.12.0-py3-none-any.whl (366 kB)\n", + " Using cached azure_storage_blob-12.12.0b1-py3-none-any.whl (365 kB)\n", + " Using cached azure_storage_blob-12.11.0-py3-none-any.whl (346 kB)\n", + "Collecting cffi>=1.12\n", + " Using cached cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (441 kB)\n", + "Collecting portalocker<3,>=1.0\n", + " Downloading portalocker-2.6.0-py2.py3-none-any.whl (15 kB)\n", + "Requirement already satisfied: jupyter-core!=5.0.*,>=4.12 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from nbclient>=0.2.0->papermill<3,>=2.1.2->feathr[notebook]) (5.1.0)\n", + "Requirement already satisfied: jupyter-client>=6.1.12 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from nbclient>=0.2.0->papermill<3,>=2.1.2->feathr[notebook]) (7.4.8)\n", + "Requirement already satisfied: traitlets>=5.3 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from nbclient>=0.2.0->papermill<3,>=2.1.2->feathr[notebook]) (5.6.0)\n", + "Collecting fastjsonschema\n", + " Using cached fastjsonschema-2.16.2-py3-none-any.whl (22 kB)\n", + "Collecting attrs>=17.4.0\n", + " Using cached attrs-22.1.0-py2.py3-none-any.whl (58 kB)\n", + "Collecting pyrsistent!=0.17.0,!=0.17.1,!=0.17.2,>=0.14.0\n", + " Downloading pyrsistent-0.19.2-py3-none-any.whl (57 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m57.5/57.5 kB\u001b[0m \u001b[31m6.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting et-xmlfile\n", + " Using cached et_xmlfile-1.1.0-py3-none-any.whl (4.7 kB)\n", + "Collecting textwrap3>=0.9.2\n", + " Using cached textwrap3-0.9.2-py2.py3-none-any.whl (12 kB)\n", + "Requirement already satisfied: tornado>=6.1 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (6.2)\n", + "Requirement already satisfied: nest-asyncio in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (1.5.6)\n", + "Requirement already satisfied: matplotlib-inline>=0.1 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (0.1.6)\n", + "Requirement already satisfied: psutil in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (5.9.4)\n", + "Requirement already satisfied: pyzmq>=17 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (24.0.1)\n", + "Requirement already satisfied: debugpy>=1.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (1.6.4)\n", + "Requirement already satisfied: pickleshare in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.7.5)\n", + "Requirement already satisfied: decorator in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (5.1.1)\n", + "Requirement already satisfied: pexpect>4.3 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (4.8.0)\n", + "Requirement already satisfied: jedi>=0.16 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.18.2)\n", + "Requirement already satisfied: stack-data in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.6.2)\n", + "Requirement already satisfied: backcall in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.2.0)\n", + "Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.11 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (3.0.33)\n", + "Requirement already satisfied: pygments>=2.4.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (2.13.0)\n", + "Collecting jupyterlab-widgets~=3.0\n", + " Using cached jupyterlab_widgets-3.0.3-py3-none-any.whl (384 kB)\n", + "Collecting widgetsnbextension~=4.0\n", + " Using cached widgetsnbextension-4.0.3-py3-none-any.whl (2.0 MB)\n", + "Collecting beautifulsoup4\n", + " Using cached beautifulsoup4-4.11.1-py3-none-any.whl (128 kB)\n", + "Collecting bleach\n", + " Using cached bleach-5.0.1-py3-none-any.whl (160 kB)\n", + "Collecting pandocfilters>=1.4.1\n", + " Using cached pandocfilters-1.5.0-py2.py3-none-any.whl (8.7 kB)\n", + "Collecting defusedxml\n", + " Using cached defusedxml-0.7.1-py2.py3-none-any.whl (25 kB)\n", + "Collecting mistune<3,>=2.0.3\n", + " Using cached mistune-2.0.4-py2.py3-none-any.whl (24 kB)\n", + "Collecting tinycss2\n", + " Using cached tinycss2-1.2.1-py3-none-any.whl (21 kB)\n", + "Collecting jupyterlab-pygments\n", + " Using cached jupyterlab_pygments-0.2.2-py2.py3-none-any.whl (21 kB)\n", + "Collecting terminado>=0.8.3\n", + " Downloading terminado-0.17.1-py3-none-any.whl (17 kB)\n", + "Collecting argon2-cffi\n", + " Using cached argon2_cffi-21.3.0-py3-none-any.whl (14 kB)\n", + "Collecting nbclassic>=0.4.7\n", + " Downloading nbclassic-0.4.8-py3-none-any.whl (9.8 MB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m9.8/9.8 MB\u001b[0m \u001b[31m14.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", + "\u001b[?25hCollecting prometheus-client\n", + " Using cached prometheus_client-0.15.0-py3-none-any.whl (60 kB)\n", + "Collecting Send2Trash>=1.8.0\n", + " Using cached Send2Trash-1.8.0-py3-none-any.whl (18 kB)\n", + "Collecting ipython-genutils\n", + " Using cached ipython_genutils-0.2.0-py2.py3-none-any.whl (26 kB)\n", + "Collecting qtpy>=2.0.1\n", + " Downloading QtPy-2.3.0-py3-none-any.whl (83 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m83.6/83.6 kB\u001b[0m \u001b[31m7.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting pycparser\n", + " Using cached pycparser-2.21-py2.py3-none-any.whl (118 kB)\n", + "Requirement already satisfied: parso<0.9.0,>=0.8.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from jedi>=0.16->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.8.3)\n", + "Requirement already satisfied: platformdirs>=2.5 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from jupyter-core!=5.0.*,>=4.12->nbclient>=0.2.0->papermill<3,>=2.1.2->feathr[notebook]) (2.5.4)\n", + "Collecting notebook-shim>=0.1.0\n", + " Downloading notebook_shim-0.2.2-py3-none-any.whl (13 kB)\n", + "Collecting jupyter-server>=1.8\n", + " Downloading jupyter_server-1.23.3-py3-none-any.whl (346 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m346.4/346.4 kB\u001b[0m \u001b[31m15.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hRequirement already satisfied: ptyprocess>=0.5 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from pexpect>4.3->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.7.0)\n", + "Requirement already satisfied: wcwidth in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from prompt-toolkit<3.1.0,>=3.0.11->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.2.5)\n", + "Collecting argon2-cffi-bindings\n", + " Using cached argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (86 kB)\n", + "Collecting soupsieve>1.2\n", + " Using cached soupsieve-2.3.2.post1-py3-none-any.whl (37 kB)\n", + "Collecting webencodings\n", + " Using cached webencodings-0.5.1-py2.py3-none-any.whl (11 kB)\n", + "Requirement already satisfied: asttokens>=2.1.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from stack-data->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (2.2.1)\n", + "Requirement already satisfied: pure-eval in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from stack-data->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.2.2)\n", + "Requirement already satisfied: executing>=1.2.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from stack-data->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (1.2.0)\n", + "Collecting websocket-client\n", + " Downloading websocket_client-1.4.2-py3-none-any.whl (55 kB)\n", + "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m55.3/55.3 kB\u001b[0m \u001b[31m5.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", + "\u001b[?25hCollecting anyio<4,>=3.1.0\n", + " Using cached anyio-3.6.2-py3-none-any.whl (80 kB)\n", + "Collecting sniffio>=1.1\n", + " Using cached sniffio-1.3.0-py3-none-any.whl (10 kB)\n", + "Building wheels for collected packages: pyspark, feathr\n", + " Building wheel for pyspark (setup.py) ... \u001b[?25ldone\n", + "\u001b[?25h Created wheel for pyspark: filename=pyspark-3.3.1-py2.py3-none-any.whl size=281845499 sha256=0f475103c6dfe6a385bf18effc5fa0fca67de759aab1f4250f202173b4b007b9\n", + " Stored in directory: /home/jumin/.cache/pip/wheels/0f/f0/3d/517368b8ce80486e84f89f214e0a022554e4ee64969f46279b\n", + " Building wheel for feathr (pyproject.toml) ... \u001b[?25ldone\n", + "\u001b[?25h Created wheel for feathr: filename=feathr-0.9.0-py3-none-any.whl size=119311 sha256=4e413243162d4ab56e4de61da2580a86fa761a76f836df8e8b9084bcf4bf0df8\n", + " Stored in directory: /tmp/pip-ephem-wheel-cache-3fzfmubz/wheels/72/6a/79/78f71da32c2bdf8534846eb5255ec37a860842063d636f9193\n", + "Successfully built pyspark feathr\n", + "Installing collected packages: webencodings, textwrap3, Send2Trash, pytz, python-snappy, py4j, mistune, ipython-genutils, fastjsonschema, confluent-kafka, azure-common, widgetsnbextension, websocket-client, urllib3, typing-extensions, tqdm, tinycss2, threadpoolctl, terminado, tenacity, tabulate, soupsieve, sniffio, pyyaml, pyspark, pyrsistent, pyparsing, pyjwt, pycparser, protobuf, prometheus-client, portalocker, pillow, pandocfilters, oauthlib, numpy, MarkupSafe, loguru, kiwisolver, jupyterlab-widgets, jupyterlab-pygments, joblib, isodate, idna, graphlib-backport, fonttools, fastavro, et-xmlfile, defusedxml, cycler, click, charset-normalizer, bleach, avro, attrs, async-timeout, ansiwrap, scipy, requests, redis, pyhocon, pyarrow, plotly, pandas, openpyxl, jsonschema, Jinja2, contourpy, cffi, beautifulsoup4, anyio, scikit-learn, requests-oauthlib, qtpy, pyapacheatlas, pandavro, nbformat, matplotlib, deltalake, databricks-cli, cryptography, azure-core, argon2-cffi-bindings, nbclient, msrest, argon2-cffi, qtconsole, papermill, nbconvert, msal, jupyter-console, ipywidgets, azure-synapse-spark, azure-storage-blob, azure-keyvault-secrets, scrapbook, msal-extensions, jupyter-server, azure-storage-file-datalake, notebook-shim, azure-identity, nbclassic, feathr, notebook, jupyter\n", + " Attempting uninstall: pyparsing\n", + " Found existing installation: pyparsing 3.0.9\n", + " Uninstalling pyparsing-3.0.9:\n", + " Successfully uninstalled pyparsing-3.0.9\n", + "Successfully installed Jinja2-3.1.2 MarkupSafe-2.1.1 Send2Trash-1.8.0 ansiwrap-0.8.4 anyio-3.6.2 argon2-cffi-21.3.0 argon2-cffi-bindings-21.2.0 async-timeout-4.0.2 attrs-22.1.0 avro-1.11.1 azure-common-1.1.28 azure-core-1.22.1 azure-identity-1.12.0 azure-keyvault-secrets-4.6.0 azure-storage-blob-12.11.0 azure-storage-file-datalake-12.5.0 azure-synapse-spark-0.7.0 beautifulsoup4-4.11.1 bleach-5.0.1 cffi-1.15.1 charset-normalizer-2.1.1 click-8.1.3 confluent-kafka-1.9.2 contourpy-1.0.6 cryptography-38.0.4 cycler-0.11.0 databricks-cli-0.17.3 defusedxml-0.7.1 deltalake-0.6.4 et-xmlfile-1.1.0 fastavro-1.5.1 fastjsonschema-2.16.2 feathr-0.9.0 fonttools-4.38.0 graphlib-backport-1.0.3 idna-3.4 ipython-genutils-0.2.0 ipywidgets-8.0.2 isodate-0.6.1 joblib-1.2.0 jsonschema-4.17.3 jupyter-1.0.0 jupyter-console-6.4.4 jupyter-server-1.23.3 jupyterlab-pygments-0.2.2 jupyterlab-widgets-3.0.3 kiwisolver-1.4.4 loguru-0.6.0 matplotlib-3.6.1 mistune-2.0.4 msal-1.20.0 msal-extensions-1.0.0 msrest-0.6.21 nbclassic-0.4.8 nbclient-0.7.2 nbconvert-7.2.6 nbformat-5.7.0 notebook-6.5.2 notebook-shim-0.2.2 numpy-1.23.5 oauthlib-3.2.2 openpyxl-3.0.10 pandas-1.5.0 pandavro-1.7.1 pandocfilters-1.5.0 papermill-2.4.0 pillow-9.3.0 plotly-5.11.0 portalocker-2.6.0 prometheus-client-0.15.0 protobuf-3.19.4 py4j-0.10.9.5 pyapacheatlas-0.14.0 pyarrow-9.0.0 pycparser-2.21 pyhocon-0.3.59 pyjwt-2.6.0 pyparsing-2.4.7 pyrsistent-0.19.2 pyspark-3.3.1 python-snappy-0.6.1 pytz-2022.6 pyyaml-6.0 qtconsole-5.4.0 qtpy-2.3.0 redis-4.4.0 requests-2.28.1 requests-oauthlib-1.3.1 scikit-learn-1.1.3 scipy-1.9.3 scrapbook-0.5.0 sniffio-1.3.0 soupsieve-2.3.2.post1 tabulate-0.9.0 tenacity-8.1.0 terminado-0.17.1 textwrap3-0.9.2 threadpoolctl-3.1.0 tinycss2-1.2.1 tqdm-4.64.1 typing-extensions-4.4.0 urllib3-1.26.13 webencodings-0.5.1 websocket-client-1.4.2 widgetsnbextension-4.0.3\n" + ] + } + ], "source": [ - "! pip install feathr azure-cli" + "# Install feathr from the latest codes in the repo. You may use `pip install feathr[notebook]` as well.\n", + "!pip install \"git+https://github.com/feathr-ai/feathr.git#subdirectory=feathr_project&egg=feathr[notebook]\" " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, @@ -74,59 +386,1970 @@ "title": "" } }, - "outputs": [], + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'sklearn'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn [1], line 9\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mazure\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39midentity\u001b[39;00m \u001b[39mimport\u001b[39;00m DefaultAzureCredential\n\u001b[1;32m 8\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mazure\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39mkeyvault\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39msecrets\u001b[39;00m \u001b[39mimport\u001b[39;00m SecretClient\n\u001b[0;32m----> 9\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39msklearn\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39mmodel_selection\u001b[39;00m \u001b[39mimport\u001b[39;00m train_test_split\n\u001b[1;32m 11\u001b[0m \u001b[39mimport\u001b[39;00m \u001b[39mfeathr\u001b[39;00m\n\u001b[1;32m 12\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mfeathr\u001b[39;00m \u001b[39mimport\u001b[39;00m (\n\u001b[1;32m 13\u001b[0m FeathrClient,\n\u001b[1;32m 14\u001b[0m STRING, BOOLEAN, FLOAT, INT32, ValueType,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 21\u001b[0m TypedKey,\n\u001b[1;32m 22\u001b[0m )\n", + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'sklearn'" + ] + } + ], "source": [ + "from datetime import datetime, timedelta\n", "import glob\n", + "from math import sqrt\n", "import os\n", "import tempfile\n", - "from datetime import datetime, timedelta\n", - "from math import sqrt\n", "\n", - "from feathr import FeathrClient\n", - "from feathr import STRING, BOOLEAN, FLOAT, INT32, ValueType\n", - "from feathr import Feature, DerivedFeature, FeatureAnchor\n", - "from feathr import BackfillTime, MaterializationSettings\n", - "from feathr import FeatureQuery, ObservationSettings\n", - "from feathr import RedisSink\n", - "from feathr import INPUT_CONTEXT, HdfsSource\n", - "from feathr import WindowAggTransformation\n", - "from feathr import TypedKey\n", - "from sklearn.model_selection import train_test_split\n", "from azure.identity import DefaultAzureCredential\n", - "from azure.keyvault.secrets import SecretClient" + "from azure.keyvault.secrets import SecretClient\n", + "from sklearn.model_selection import train_test_split\n", + "\n", + "import feathr\n", + "from feathr import (\n", + " FeathrClient,\n", + " STRING, BOOLEAN, FLOAT, INT32, ValueType,\n", + " Feature, DerivedFeature, FeatureAnchor,\n", + " BackfillTime, MaterializationSettings,\n", + " FeatureQuery, ObservationSettings,\n", + " RedisSink,\n", + " HdfsSource,\n", + " WindowAggTransformation,\n", + " TypedKey,\n", + ")\n", + "from feathr.utils.config import generate_config\n", + "\n", + "\n", + "print(f\"Feathr version: {feathr.__version__}\")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "c0299d67-1103-4aa4-ba57-300498ae2579", - "showTitle": false, - "title": "" - } + "tags": [ + "parameters" + ] }, "outputs": [], "source": [ - "! az login --use-device-code" + "RESOURCE_PREFIX = None # TODO fill the value used to deploy the resources via ARM template\n", + "PROJECT_NAME = \"fraud_detection\"\n", + "\n", + "# Currently support: 'azure_synapse', 'databricks', and 'local' \n", + "SPARK_CLUSTER = \"local\"\n", + "\n", + "# TODO fill values to use databricks cluster:\n", + "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", + "DATABRICKS_URL = None # Set Databricks workspace url to use databricks\n", + "DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", + "\n", + "# TODO fill values to use Azure Synapse cluster:\n", + "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", + "AZURE_SYNAPSE_URL = None # Set Azure Synapse workspace url to use Azure Synapse\n", + "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", + "\n", + "# An existing Feathr config file path. If None, we'll generate a new config based on the constants in this cell.\n", + "FEATHR_CONFIG_PATH = None\n", + "\n", + "# If set True, use an interactive browser authentication to get the redis password.\n", + "USE_CLI_AUTH = False" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO remove this cell\n", + "RESOURCE_PREFIX = \"juntest\"\n", + "USE_CLI_AUTH = True" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "if SPARK_CLUSTER == \"azure_synapse\" and not os.environ.get(\"ADLS_KEY\"):\n", + " os.environ[\"ADLS_KEY\"] = ADLS_KEY\n", + "elif SPARK_CLUSTER == \"databricks\" and not os.environ.get(\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"):\n", + " os.environ[\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"] = DATABRICKS_WORKSPACE_TOKEN_VALUE" + ] + }, + { + "cell_type": "code", + "execution_count": 15, "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, - "nuid": "58291272-00e5-4fe3-99d6-f1b89726f692", + "nuid": "c0299d67-1103-4aa4-ba57-300498ae2579", "showTitle": false, "title": "" } }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mTo sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code R6F3RHUP2 to authenticate.\u001b[0m\n", + "\u001b[33mFailed to authenticate '{'additional_properties': {}, 'id': '/tenants/d3e49573-1ecc-4a43-adfb-7400029d7049', 'tenant_id': 'd3e49573-1ecc-4a43-adfb-7400029d7049', 'tenant_category': 'Home', 'country': None, 'country_code': None, 'display_name': None, 'domains': None}' due to error 'Get Token request returned http error: 400 and server response: {\"error\":\"invalid_grant\",\"error_description\":\"AADSTS50020: User account '{EmailHidden}' from identity provider 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/' does not exist in tenant 'iconfitness.com' and cannot access the application '04b07795-8ddb-461a-bbee-02f9e1bf7b46'(Microsoft Azure CLI) in that tenant. The account needs to be added as an external user in the tenant first. Sign out and sign in again with a different Azure Active Directory user account.\\r\\nTrace ID: 0e29aa07-07fb-4843-a292-35074eda1d00\\r\\nCorrelation ID: cb2831fd-6b52-4bcb-bc90-d744682c896c\\r\\nTimestamp: 2022-12-06 22:22:42Z\",\"error_codes\":[50020],\"timestamp\":\"2022-12-06 22:22:42Z\",\"trace_id\":\"0e29aa07-07fb-4843-a292-35074eda1d00\",\"correlation_id\":\"cb2831fd-6b52-4bcb-bc90-d744682c896c\",\"error_uri\":\"https://login.microsoftonline.com/error?code=50020\"}'\u001b[0m\n", + "\u001b[33mThe following tenants don't contain accessible subscriptions. Use 'az login --allow-no-subscriptions' to have tenant level access.\u001b[0m\n", + "\u001b[33m985de3af-81be-4db4-b2e4-b6da729941fe 'Azure Global Critical Infrastructure'\u001b[0m\n", + "[\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"a1ffc958-d2c7-493e-9f1e-125a0477f536\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"MSFT-MVD-05-Shared-EUDB\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"2f96ae42-240b-4228-bafa-26d8b7b03bf3\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"MSFT-CloudMS-CPT-PRD-01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"8c40f547-9775-44d6-bb8b-19f20d050dd2\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"PlayFab.Analytics.Dev\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"94e8b8dd-5fb0-40f9-beb3-5b63eeb2aacc\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"PlayFab.PlayStream.NonProd\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"b10f7f41-91c1-4bdb-8d39-fe7feaa39d27\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"PlayFab.Analytics.Prod\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"e7348740-ed0e-4192-a663-dc7d353e311a\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"PlayFab.Insights.Essentials.Dev\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"5358a7bb-87ab-40e1-af58-c45f3657cdea\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"PlayFab.Insights.Dev\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"b5cb67a7-b716-42db-8ab5-203713124d5e\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"PlayFab.Stack.CICD.Stamp\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"7a1b56af-d64b-4525-b458-592a9f131a81\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"PlayFab.Stack.Dev.Partition\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"f5a7afd0-c585-4f46-b011-4024e1036293\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"GDX.PlayFab.Cerulean.Test.Dev\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"aad30b49-5326-4f4d-b7bd-8d408f5051e2\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"GDX.PlayFab.Cerulean.Test.Dev2\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"0ca618d2-22a8-413a-96d0-0f1b531129c3\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"Azure-Gaming-Private\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"6187b663-b744-4d24-8226-7e66525baf8f\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"AG-CI-AI-NI-SUPPLYCHAIN\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"2eedf122-c960-4ddc-9146-ac93dbf8b2b0\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"GAMING_PERSONALIZATION\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"95f31ea2-0e41-4d66-a5db-9ef0449ad928\",\n", + " \"isDefault\": true,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"AGS-GAME-TESTING\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"a11db6f9-92cb-4ef3-88ca-11c0d1e3789e\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.Community.Metrics.Prod03\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"b319454f-e9f9-4c54-be5c-d848e7a3d565\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.GXP.Metrics.Prod01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"8eccbb05-b393-4d06-9056-b9cd302590e6\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.GXP.Metrics.NonProd01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"d29ae919-0b7c-49d8-92a7-44cb3d3ff331\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.xGO.Metrics.Prod01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"ac6239b8-c012-498d-9241-37b92bdb091d\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.xGO.Metrics.NonProd01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"32fcca0f-6263-4450-b688-4737dc916f7d\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.GDX.Metrics.Prod01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"ea92ec9c-db3a-4d5d-8dea-cbc66e40d3aa\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.GDX.Metrics.NonProd01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"03aeb9f7-141e-4ec2-8921-1c6d4a69601f\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.Community.Metrics.Prod02\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"917d3e55-25a5-48ee-8a88-7c123c48444a\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.Community.Metrics.Prod01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"a05f3621-3dda-4ab6-8325-3e1e2dea1fe9\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.Community.Metrics.NonProd02\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"43a7747e-fad4-447d-a2e1-bacc1d19ff1d\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.Community.Metrics.NonProd01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"73ae2794-39e5-4e07-b2db-c256d950eaa7\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.Community.Kusto.Prod01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"387e5205-b3bb-42b2-83be-108fef1dd57a\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Gaming.Community.Kusto.NonProd01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"bac420ed-c6fc-4a05-8ac1-8c0c52da1d6e\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"IDEAs MS Reporting\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"dd4c2dac-db51-4cd0-b734-684c6cc360c1\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"Geneva Analytics Intelligent Data Test (Corp)\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"8c4b5b03-3b24-4ed0-91f5-a703cd91b412\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Cosmos_C&E_Azure_AzureEngineeringSystems_100200\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"79087098-5ecd-41bb-bc17-dd5c35aab810\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"AzPentest Sub 3\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"1278a874-89fc-418c-b6b9-ac763b000415\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"(NBrady) Microsoft Azure Internal Consumption\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"e686ef8c-d35d-4e9b-92f8-caaaa7948c0a\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Applied Sciences Group Dev\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"54b875cc-a81a-4914-8bfd-1a36bc7ddf4d\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"MSFT-WindowsVirtualDesktop-01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"c3547baf-332f-4d8f-96bd-0659b39c7a59\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"MSAI Search Research\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"407ff5d7-0113-4c5c-8534-f5cfb09298f5\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"msteams.nonprod.pub.msft.msts\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"588845a8-a4a7-4ab1-83a1-1388452e8c0c\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Saidi Dev and Test Subscription\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"dad45786-32e5-4ef3-b90e-8e0838fbadb6\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"AnE.ExP.Production\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"c60e2ea3-34e5-4667-95e4-d7da5166d8c8\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"ELITE_E360_PROD_R&D_01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"03bb6720-b3b0-45b0-9c37-748c34c86ad7\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"DS-SQL-CHOICE_HelsinkiLab_slavao_60843\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"21845c4f-ca8c-47bc-a3c5-12b1c15adccf\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"FIG Azure Services\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"09b5fdb3-165d-4e2b-8ca0-34f998d176d5\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"GameStreaming.Data.PROD.01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"b43a6159-1bea-4fa2-9407-e875fdc0ff55\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"BAP IA EU Schrems (P)\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"0f301386-8979-4981-acca-973d553078e7\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Scope Team Test Subscription\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"f30787b9-82a8-4e74-bb0f-f12d64ecc496\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Edge ExpInsights\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"5bcfd686-e65f-4381-94bd-44521888475c\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Epic-Edge-ES-MacLab-Dev\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"82ee6f14-d8a6-4395-97cd-4db8118bbe1b\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Epic-Edge-ES-MacLab-Prod\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"333e402a-65a0-45a9-8e23-867ca146c290\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Epic-Edge-ES-ReproVMLab\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"482e1993-01d4-4b16-bff4-1866929176a1\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Epic-Edge-ES-MergeResolver\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"26596251-f2f3-4e31-8a1b-f0754e32ad73\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Epic-Edge-ES-SrcPub-Prod\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"2876b824-a621-4dff-99b8-2fdb1a727b02\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"COSINE-ES-BS-DPB-BuildServices\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"e01de573-132a-42ac-9ee2-f9dea9dd2717\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Cosmos_WDG_Core_BnB_100292\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"4115b323-4aac-47f4-bb13-22af265ed58b\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"ES-ESP-AS-vPack-Nonprod\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"9ec1d932-0f3f-486c-acc6-e7d78b358f9b\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"TScience\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"8c2c7b23-848d-40fe-b817-690d79ad9dfd\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"Purview_Demo_PartnerSandbox\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"29de2cfc-f00a-43bb-bdc8-3108795bd282\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"MSFT-Modern Device-Modern Mgmt-Imaging00\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"7afc2d66-d5b4-4e84-970b-a782e3e4cc46\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"CLOUDBUILD-ANYBUILD-POC-01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"7d411d23-59e5-4e2e-8566-4f59de4544f2\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Dev:ER: CE Security Service Operation Center\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"0368444d-756e-4ca6-9ecd-e964248c227a\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"AnyBuild-InternalProdClusters\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"cb9eb375-570a-4e75-b83a-77dd942bee9f\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"PPP_ACBB_ATS_AKS_NonProd\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"009d0e9f-a42a-470e-b315-82496a88cf0f\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Weather and Finance INT\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"6f53185c-ea09-4fc3-9075-318dec805303\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"33e01921-4d64-4f8c-a055-5bdaffd5e33d\"\n", + " }\n", + " ],\n", + " \"name\": \"RPSAAS CSM Dogfood\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"dcbdac96-1896-478d-89fc-c95ed43f4596\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"pgattikoppul Projects\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"3b2fda06-3ef6-454a-9dd5-994a548243e9\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"Microsoft Employee & Executive Communications\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"227531a4-d775-435b-a878-963ed8d0d18f\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"cdc5aeea-15c5-4db6-b079-fcadd2505dc2\"\n", + " },\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"Skype-1ES-DEV\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"432a7068-99ae-4975-ad38-d96b71172cdf\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Azure Profiler - Testing\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"0cd6095b-b140-41ec-ad1d-32f2f7493386\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"1IS - One Integration Service - NonProduction\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"4628298e-882d-4f12-abf4-a9f9654960bb\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"ES_DeveloperProductivity_Dev\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"c0620f27-ac38-468c-a26b-264009fe7c41\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"AzureQualityInfrastructureSubscription-Test00\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"b68b2f37-1d37-4c2f-80f6-c23de402792e\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"Semantic Machines R&D Subscription 1\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"e4e06275-58d1-4081-8f1b-be12462eb701\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"CSR-CUSPOC-MST-Hiramfleitas\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"3cd95ff9-ac62-4b5c-8240-0cd046687ea0\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Internal App Insights Resources for Perflens\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"bbe41737-1ade-44df-8e33-217f11b8b452\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Service Profiler Development\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"1163fbbe-27e7-4b0f-8466-195fe5417043\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " },\n", + " {\n", + " \"tenantId\": \"d0ecd01b-d782-448e-bae0-c3cad0e0543a\"\n", + " },\n", + " {\n", + " \"tenantId\": \"94c4857e-1130-4ab8-8eac-069b40c9db20\"\n", + " },\n", + " {\n", + " \"tenantId\": \"f702a9dc-ae48-4dc7-8f0a-8155a6dfa4e5\"\n", + " }\n", + " ],\n", + " \"name\": \"Python_AI_Tools_PM_Team_Sub\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"8ae1955e-f748-4273-a507-10159ba940f9\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"SPO-CyrusB\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"9b4236fe-df75-4289-bf00-40628ed41fd9\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Customer Confidence Proof of Concepts\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"13723929-6644-4060-a50a-cc38ebc5e8b1\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"LinuxMdsd Test\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"10d0b7c6-9243-4713-91a9-2730375d3a1b\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"ASutton Subscription\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"3905431d-c062-4c17-8fd9-c51f89f334c4\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Pytorch Build\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"9372b318-ed3a-4504-95a6-941201300f78\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"OE-Service Management Platform - External - Prod\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"e0fd569c-e34a-4249-8c24-e8d723c7f054\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"AML - Responsible AI R&D\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"56207a5d-4746-4a2d-9f7f-c5ac77a5ddfd\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"Traffic manager Preview\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"bac044cf-49e1-4843-8dda-1ce9662606c8\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"UI Fabric\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"ca38bc19-cf50-48e2-bbe6-8c35b40212d8\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Maps Mining Azure\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"b2a328a7-ffff-4c09-b643-a4758cf170bc\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"AISC-DEV-02\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"85b3dbca-5974-4067-9669-67a141095a76\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"027fe226-784c-4798-a157-5e0e7893424d\"\n", + " }\n", + " ],\n", + " \"name\": \"Terraform Testing on Azure with TTL = 2 Days\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"8cff5d56-95fb-4a74-ab9d-079edb45313e\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"cdc5aeea-15c5-4db6-b079-fcadd2505dc2\"\n", + " }\n", + " ],\n", + " \"name\": \"Skype-NetEM-STAGING\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"68f3658f-0090-4277-a500-f02227aaee97\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"zejunz\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"79f57c16-00fe-48da-87d4-5192e86cd047\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"TScienceGPU\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"ae71ef11-a03f-4b4f-a0e6-ef144727c711\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"Bing MM Measurement\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"eec2de82-6ab2-4a84-ae5f-57e9a10bf661\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"ServicesPortfolio MCS\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"e72e5254-f265-4e95-9bd2-9ee8e7329051\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Speech Services - DEV - SDK (rob)\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"eef8b6d5-94da-4b36-9327-a662f2674efb\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"AISC-EngSys-01\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"ad203158-bc5d-4e72-b764-2607833a71dc\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"Project Vienna Build\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"d2c9544f-4329-4642-b73d-020e7fef844f\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"HPCScrub1\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"50ff7bc0-cd15-49d5-abb2-e975184c2f65\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"IDXXpertRandD Dev Data Catalog\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"4bbecc02-f2c3-402a-8e01-1dfb1ffef499\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Azure Portal Telemetry Reporting\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"5e552837-76cb-48b1-8983-b12ca284552b\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"jawelch\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"caffe3c0-acbd-4d01-af76-a45f421bfb64\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Azure_Base_LivesiteArmory_test\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"38d911df-2706-4195-8563-c75b49f7a88d\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Actions on AKS Dev\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"de6bc98d-2a4d-4607-919e-67692d1eba2b\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Dart Eng Beta\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"b8faf5eb-39a1-42c4-9e2a-335704d4c740\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"VSO-SD-POC\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"84ca48fe-c942-42e5-b492-d56681d058fa\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"AEP_CorePlatform_Playground_Dev\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"0ee78edb-a0ad-456c-a0a2-901bf542c102\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"ADF Test sub - App Model V2\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"8168a4f2-74d6-4663-9951-8e3a454937b7\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"TEST - IbizaFx misc\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"cd0fa82d-b6b6-4361-b002-050c32f71353\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Falcon Dev Cluster\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"b5c0b80f-5932-4d47-ae25-cd617dac90ce\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"CRM-DEVTEST-Efun-IDC\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"6560575d-fa06-4e7d-95fb-f962e74efd7a\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"ML Lifecycle PM\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"8d5565a3-dec1-4ee2-86d6-8aabb315eec4\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"Gandalf Analytics Service - Stage\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"d03b04c7-d1d4-467b-aaaa-87b6fcb38b38\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"VSEng Shared\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"bd62906c-0a81-43c3-a2f8-126e4cf66ada\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"DevDiv Key Vault\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"26b9b438-7fe8-482f-b732-ea99c70f2abb\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"ddverify\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"8bc2f89b-c4f6-4559-ad6a-4f2cfa6ccc49\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"VSEng MadDog-RPS Telemetry\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"eb87f285-893a-4f0f-8c55-7b4f67b1d097\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Dart Eng Alpha\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"58706c15-d8a1-4397-af3c-e1ebf1cbebe5\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"RPS-cloud-common-2\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"52a442a2-31e9-42f9-8e3e-4b27dbf82673\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"Core-ES-WorkManagement\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"365a62ee-6166-4d37-a936-03585106dd50\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"NetVal Dev Sandbox\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"a6c2a7cc-d67e-4a1a-b765-983f08c0423a\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " },\n", + " {\n", + " \"tenantId\": \"f40b18ba-b66c-49e4-9fd8-4fc7d3d19f0f\"\n", + " }\n", + " ],\n", + " \"name\": \"AI Customer Engineering - Internal Consumption\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"f8d440ed-264b-4a91-8ab9-0b1694914abf\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [],\n", + " \"name\": \"AGS_GamingProjects_Dev\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " },\n", + " {\n", + " \"cloudName\": \"AzureCloud\",\n", + " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"id\": \"c9126719-f58d-4455-a79c-6ddc2c326802\",\n", + " \"isDefault\": false,\n", + " \"managedByTenants\": [\n", + " {\n", + " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", + " }\n", + " ],\n", + " \"name\": \"ACE Customer Projects\",\n", + " \"state\": \"Enabled\",\n", + " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", + " \"user\": {\n", + " \"name\": \"jumin@microsoft.com\",\n", + " \"type\": \"user\"\n", + " }\n", + " }\n", + "]\n", + "\u001b[0m" + ] + } + ], "source": [ - "# replace with your prefix\n", - "resource_prefix = " + "if USE_CLI_AUTH:\n", + " !az login --use-device-code" ] }, { @@ -151,7 +2374,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, @@ -162,38 +2385,31 @@ }, "outputs": [], "source": [ - "# Get all the required credentials from Azure Key Vault\n", - "key_vault_name=resource_prefix+\"kv\"\n", - "synapse_workspace_url=resource_prefix+\"syws\"\n", - "adls_account=resource_prefix+\"dls\"\n", - "adls_fs_name=resource_prefix+\"fs\"\n", - "purview_name=resource_prefix+\"purview\"\n", - "key_vault_uri = f\"https://{key_vault_name}.vault.azure.net\"\n", - "credential = DefaultAzureCredential(exclude_interactive_browser_credential=False, additionally_allowed_tenants=['*'])\n", - "client = SecretClient(vault_url=key_vault_uri, credential=credential)\n", - "secretName = \"FEATHR-ONLINE-STORE-CONN\"\n", - "retrieved_secret = client.get_secret(secretName).value\n", + "# Redis password\n", + "if 'REDIS_PASSWORD' not in os.environ:\n", + " # Try to get all the required credentials from Azure Key Vault\n", + " from azure.identity import AzureCliCredential, DefaultAzureCredential \n", + " from azure.keyvault.secrets import SecretClient\n", "\n", - "# Get redis credentials; This is to parse Redis connection string.\n", - "redis_port=retrieved_secret.split(',')[0].split(\":\")[1]\n", - "redis_host=retrieved_secret.split(',')[0].split(\":\")[0]\n", - "redis_password=retrieved_secret.split(',')[1].split(\"password=\",1)[1]\n", - "redis_ssl=retrieved_secret.split(',')[2].split(\"ssl=\",1)[1]\n", + " # TODO assume the resources are deployed by using the ARM template. If not, please set your vault url name.\n", + " vault_url = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", + " if USE_CLI_AUTH:\n", + " credential = AzureCliCredential(additionally_allowed_tenants=['*'],)\n", + " else:\n", + " credential = DefaultAzureCredential(\n", + " exclude_interactive_browser_credential=False,\n", + " additionally_allowed_tenants=['*'],\n", + " )\n", + " secret_client = SecretClient(vault_url=vault_url, credential=credential)\n", + " retrieved_secret = secret_client.get_secret('FEATHR-ONLINE-STORE-CONN').value\n", + " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]\n", "\n", - "# Set the resource link\n", - "os.environ['spark_config__azure_synapse__dev_url'] = f'https://{synapse_workspace_url}.dev.azuresynapse.net'\n", - "os.environ['spark_config__azure_synapse__pool_name'] = 'spark31'\n", - "os.environ['spark_config__azure_synapse__workspace_dir'] = f'abfss://{adls_fs_name}@{adls_account}.dfs.core.windows.net/feathr_project'\n", - "os.environ['online_store__redis__host'] = redis_host\n", - "os.environ['online_store__redis__port'] = redis_port\n", - "os.environ['online_store__redis__ssl_enabled'] = redis_ssl\n", - "os.environ['REDIS_PASSWORD']=redis_password\n", - "feathr_output_path = f'abfss://{adls_fs_name}@{adls_account}.dfs.core.windows.net/feathr_output'" + "# feathr_output_path = f'abfss://{adls_fs_name}@{adls_account}.dfs.core.windows.net/feathr_output'" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, @@ -202,59 +2418,49 @@ "title": "" } }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "api_version: 1\n", + "feature_registry:\n", + " api_endpoint: https://juntestwebapp.azurewebsites.net/api/v1\n", + "offline_store:\n", + " adls:\n", + " adls_enabled: 'true'\n", + " wasb:\n", + " wasb_enabled: 'true'\n", + "online_store:\n", + " redis:\n", + " host: juntestredis.redis.cache.windows.net\n", + " port: '6380'\n", + " ssl_enabled: 'true'\n", + "project_config:\n", + " project_name: fraud_detection\n", + "spark_config:\n", + " spark_cluster: local\n", + " spark_result_output_parts: '1'\n", + "\n" + ] + } + ], "source": [ - "import tempfile\n", - "yaml_config = \"\"\"\n", - "# Please refer to https://github.com/feathr-ai/feathr/blob/main/feathr_project/feathrcli/data/feathr_user_workspace/feathr_config.yaml for explanations on the meaning of each field.\n", - "api_version: 1\n", - "project_config:\n", - " project_name: 'fraud_detection_test'\n", - " required_environment_variables:\n", - " - 'REDIS_PASSWORD'\n", - "offline_store:\n", - "# Please set 'enabled' flags as true (false by default) if any of items under the same paths are expected to be visited\n", - " adls:\n", - " adls_enabled: true\n", - " wasb:\n", - " wasb_enabled: true\n", - " s3:\n", - " s3_enabled: false\n", - " s3_endpoint: ''\n", - " jdbc:\n", - " jdbc_enabled: false\n", - " jdbc_database: ''\n", - " jdbc_table: ''\n", - " snowflake:\n", - " snowflake_enabled: false\n", - " url: \".snowflakecomputing.com\"\n", - " user: \"\"\n", - " role: \"\"\n", - " warehouse: \"\"\n", - "spark_config:\n", - " spark_cluster: 'azure_synapse'\n", - " spark_result_output_parts: '1'\n", - " azure_synapse:\n", - " dev_url: 'https://.dev.azuresynapse.net'\n", - " pool_name: 'spark3'\n", - " workspace_dir: 'abfss://{adls_fs_name}@{adls_account}.dfs.core.windows.net/fraud_detection_test'\n", - " executor_size: 'Small'\n", - " executor_num: 1\n", - " databricks:\n", - " workspace_instance_url: 'https://.azuredatabricks.net'\n", - " config_template: {'run_name':'','new_cluster':{'spark_version':'9.1.x-scala2.12','node_type_id':'Standard_D3_v2','num_workers':2,'spark_conf':{}},'libraries':[{'jar':''}],'spark_jar_task':{'main_class_name':'','parameters':['']}}\n", - " work_dir: 'dbfs:/fraud_detection_test'\n", - "online_store:\n", - " redis:\n", - " host: '.redis.cache.windows.net'\n", - " port: 6380\n", - " ssl_enabled: True\n", - "feature_registry:\n", - " api_endpoint: \"https://.azurewebsites.net/api/v1\"\n", - "\"\"\"\n", - "tmp = tempfile.NamedTemporaryFile(mode='w', delete=False)\n", - "with open(tmp.name, \"w\") as text_file:\n", - " text_file.write(yaml_config)\n" + "if FEATHR_CONFIG_PATH:\n", + " config_path = FEATHR_CONFIG_PATH\n", + "else:\n", + " config_path = generate_config(\n", + " resource_prefix=RESOURCE_PREFIX,\n", + " project_name=PROJECT_NAME,\n", + " spark_config__spark_cluster=SPARK_CLUSTER,\n", + " spark_config__azure_synapse__dev_url=AZURE_SYNAPSE_URL,\n", + " spark_config__azure_synapse__pool_name=AZURE_SYNAPSE_SPARK_POOL,\n", + " spark_config__databricks__workspace_instance_url=DATABRICKS_URL,\n", + " databricks_cluster_id=DATABRICKS_CLUSTER_ID,\n", + " )\n", + "\n", + "with open(config_path, 'r') as f: \n", + " print(f.read())" ] }, { @@ -274,7 +2480,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, @@ -283,9 +2489,25 @@ "title": "" } }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2022-12-06 12:39:40.576 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - secrets__azure_key_vault__name not found in the config file.\n", + "2022-12-06 12:39:40.600 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__s3__s3_enabled not found in the config file.\n", + "2022-12-06 12:39:40.619 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__jdbc__jdbc_enabled not found in the config file.\n", + "2022-12-06 12:39:40.624 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__snowflake__snowflake_enabled not found in the config file.\n", + "2022-12-06 12:39:40.634 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__feathr_runtime_location not found in the config file.\n", + "2022-12-06 12:39:40.640 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__workspace not found in the config file.\n", + "2022-12-06 12:39:40.644 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__master not found in the config file.\n", + "2022-12-06 12:39:40.653 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - feature_registry__purview__purview_name not found in the config file.\n", + "2022-12-06 12:39:40.655 | INFO | feathr.client:__init__:196 - Feathr client 0.9.0 initialized successfully.\n" + ] + } + ], "source": [ - "client = FeathrClient(config_path=tmp.name, credential=credential)" + "client = FeathrClient(config_path=config_path, credential=credential)" ] }, { @@ -307,6 +2529,51 @@ "- `DerivedFeature`" ] }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "from feathr.datasets.constants import FRAUD_DETECTION_ACCOUNT_INFO_URL, FRAUD_DETECTION_TRANSACTIONS_URL\n", + "\n", + "from feathr.datasets.utils import maybe_download\n", + "\n", + "from pathlib import Path" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'PROJECT_NAME' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn [15], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[39m# upload dataset if needed\u001b[39;00m\n\u001b[0;32m----> 3\u001b[0m account_info_file_path \u001b[39m=\u001b[39m \u001b[39mstr\u001b[39m(Path(PROJECT_NAME, \u001b[39m\"\u001b[39m\u001b[39maccount_info.csv\u001b[39m\u001b[39m\"\u001b[39m))\n\u001b[1;32m 4\u001b[0m transactions_file_path \u001b[39m=\u001b[39m \u001b[39mstr\u001b[39m(Path(PROJECT_NAME, \u001b[39m\"\u001b[39m\u001b[39mtransactions.csv\u001b[39m\u001b[39m\"\u001b[39m))\n\u001b[1;32m 5\u001b[0m maybe_download(\n\u001b[1;32m 6\u001b[0m src_url\u001b[39m=\u001b[39mFRAUD_DETECTION_ACCOUNT_INFO_URL,\n\u001b[1;32m 7\u001b[0m dst_filepath\u001b[39m=\u001b[39maccount_info_file_path,\n\u001b[1;32m 8\u001b[0m )\n", + "\u001b[0;31mNameError\u001b[0m: name 'PROJECT_NAME' is not defined" + ] + } + ], + "source": [ + "# upload dataset if needed\n", + "\n", + "account_info_file_path = str(Path(PROJECT_NAME, \"account_info.csv\"))\n", + "transactions_file_path = str(Path(PROJECT_NAME, \"transactions.csv\"))\n", + "maybe_download(\n", + " src_url=FRAUD_DETECTION_ACCOUNT_INFO_URL,\n", + " dst_filepath=account_info_file_path,\n", + ")\n", + "maybe_download(\n", + " src_url=FRAUD_DETECTION_TRANSACTIONS_URL,\n", + " dst_filepath=transactions_file_path,\n", + ")" + ] + }, { "cell_type": "markdown", "metadata": { @@ -335,10 +2602,12 @@ "outputs": [], "source": [ "#Refer to to learn more about the details of each method\n", - "account_info = HdfsSource(name=\"AccountData\",\n", - " path=\"wasbs://frauddata@feathrdatastorage.blob.core.windows.net/account_out_small.csv\",\n", - " event_timestamp_column=\"transactionDate\",\n", - " timestamp_format=\"yyyyMMdd\")\n", + "account_info = HdfsSource(\n", + " name=\"AccountData\",\n", + " path=\"wasbs://frauddata@feathrdatastorage.blob.core.windows.net/account_out_small.csv\",\n", + " event_timestamp_column=\"transactionDate\",\n", + " timestamp_format=\"yyyyMMdd\",\n", + ")\n", "\n", "accountId = TypedKey(key_column=\"accountID\",\n", " key_column_type=ValueType.INT32,\n", @@ -998,7 +3267,7 @@ "widgets": {} }, "kernelspec": { - "display_name": "Python 3.9.14 64-bit", + "display_name": "Python 3.10.4 ('feathr')", "language": "python", "name": "python3" }, @@ -1012,12 +3281,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.14" + "version": "3.10.4" }, "orig_nbformat": 4, "vscode": { "interpreter": { - "hash": "a665b5d41d17b532ea9890333293a1b812fa0b73c9c25c950b3cedf1bebd0438" + "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" } } }, diff --git a/docs/samples/nyc_taxi_demo.ipynb b/docs/samples/nyc_taxi_demo.ipynb index eb83dd118..78a6cfaec 100644 --- a/docs/samples/nyc_taxi_demo.ipynb +++ b/docs/samples/nyc_taxi_demo.ipynb @@ -63,7 +63,7 @@ "source": [ "## 1. Install Feathr and Necessary Dependancies\n", "\n", - "Install feathr and necessary packages by running `pip install feathr[notebook]` if you haven't installed them already." + "Install feathr and necessary packages by running one of following commends if you haven't installed them already:" ] }, { @@ -71,6 +71,19 @@ "execution_count": null, "metadata": {}, "outputs": [], + "source": [ + "# To install feathr from the latest codes in the repo:\n", + "# !pip install \"git+https://github.com/feathr-ai/feathr.git#subdirectory=feathr_project&egg=feathr[notebook]\" \n", + "\n", + "# To install the latest release:\n", + "# !pip install feathr[notebook] " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], "source": [ "%load_ext autoreload\n", "%autoreload 2" @@ -154,12 +167,14 @@ "SPARK_CLUSTER = \"local\"\n", "\n", "# TODO fill values to use databricks cluster:\n", - "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", - "DATABRICKS_URL = None # Set Databricks workspace url to use databricks\n", + "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", + "DATABRICKS_URL = None # Set Databricks workspace url to use databricks\n", + "DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", "\n", "# TODO fill values to use Azure Synapse cluster:\n", "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", - "AZURE_SYNAPSE_URL = None # Set Azure Synapse workspace url to use Azure Synapse\n", + "AZURE_SYNAPSE_URL = None # Set Azure Synapse workspace url to use Azure Synapse\n", + "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", "\n", "# Data store root path. Could be a local file system path, dbfs or Azure storage path like abfs or wasbs\n", "DATA_STORE_PATH = TemporaryDirectory().name\n", @@ -207,9 +222,9 @@ "outputs": [], "source": [ "if SPARK_CLUSTER == \"azure_synapse\" and not os.environ.get(\"ADLS_KEY\"):\n", - " os.environ[\"ADLS_KEY\"] = add_your_key_here\n", + " os.environ[\"ADLS_KEY\"] = ADLS_KEY\n", "elif SPARK_CLUSTER == \"databricks\" and not os.environ.get(\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"):\n", - " os.environ[\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"] = add_your_token_here" + " os.environ[\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"] = DATABRICKS_WORKSPACE_TOKEN_VALUE" ] }, { @@ -1107,7 +1122,7 @@ }, "celltoolbar": "Tags", "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3.10.4 ('feathr')", "language": "python", "name": "python3" }, @@ -1125,7 +1140,7 @@ }, "vscode": { "interpreter": { - "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" + "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" } } }, diff --git a/feathr_project/feathr/datasets/constants.py b/feathr_project/feathr/datasets/constants.py index 849865570..873afe341 100644 --- a/feathr_project/feathr/datasets/constants.py +++ b/feathr_project/feathr/datasets/constants.py @@ -1,3 +1,11 @@ NYC_TAXI_SMALL_URL = ( "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/green_tripdata_2020-04_with_index.csv" ) + +FRAUD_DETECTION_ACCOUNT_INFO_URL = ( + "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/frauddetection/Account_Info.csv" +) + +FRAUD_DETECTION_TRANSACTIONS_URL = ( + "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/frauddetection/Fraud_Transactions.csv" +) diff --git a/feathr_project/setup.py b/feathr_project/setup.py index cc6f9e498..979a77e7a 100644 --- a/feathr_project/setup.py +++ b/feathr_project/setup.py @@ -35,6 +35,7 @@ "pytest-mock>=3.8.1", ], notebook=[ + "azure-cli", # Azure CLI credentials "jupyter==1.0.0", "matplotlib==3.6.1", "papermill>=2.1.2,<3", # to test run notebooks From 62d750982675c48323a1a513adf098fd96937548 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Wed, 7 Dec 2022 20:51:15 +0000 Subject: [PATCH 02/22] wip. Remove azure cli package from extra dependencies Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/fraud_detection_demo.ipynb | 1948 +---------------------- docs/samples/nyc_taxi_demo.ipynb | 8 +- feathr_project/setup.py | 5 +- 3 files changed, 64 insertions(+), 1897 deletions(-) diff --git a/docs/samples/fraud_detection_demo.ipynb b/docs/samples/fraud_detection_demo.ipynb index fc5993d22..baa7e031d 100644 --- a/docs/samples/fraud_detection_demo.ipynb +++ b/docs/samples/fraud_detection_demo.ipynb @@ -388,14 +388,10 @@ }, "outputs": [ { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'sklearn'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn [1], line 9\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mazure\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39midentity\u001b[39;00m \u001b[39mimport\u001b[39;00m DefaultAzureCredential\n\u001b[1;32m 8\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mazure\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39mkeyvault\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39msecrets\u001b[39;00m \u001b[39mimport\u001b[39;00m SecretClient\n\u001b[0;32m----> 9\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39msklearn\u001b[39;00m\u001b[39m.\u001b[39;00m\u001b[39mmodel_selection\u001b[39;00m \u001b[39mimport\u001b[39;00m train_test_split\n\u001b[1;32m 11\u001b[0m \u001b[39mimport\u001b[39;00m \u001b[39mfeathr\u001b[39;00m\n\u001b[1;32m 12\u001b[0m \u001b[39mfrom\u001b[39;00m \u001b[39mfeathr\u001b[39;00m \u001b[39mimport\u001b[39;00m (\n\u001b[1;32m 13\u001b[0m FeathrClient,\n\u001b[1;32m 14\u001b[0m STRING, BOOLEAN, FLOAT, INT32, ValueType,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 21\u001b[0m TypedKey,\n\u001b[1;32m 22\u001b[0m )\n", - "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'sklearn'" + "name": "stdout", + "output_type": "stream", + "text": [ + "Feathr version: 0.9.0\n" ] } ], @@ -430,7 +426,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 2, "metadata": { "tags": [ "parameters" @@ -463,18 +459,18 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "# TODO remove this cell\n", "RESOURCE_PREFIX = \"juntest\"\n", - "USE_CLI_AUTH = True" + "USE_CLI_AUTH = False" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -486,7 +482,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 8, "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, @@ -495,1858 +491,7 @@ "title": "" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[33mTo sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code R6F3RHUP2 to authenticate.\u001b[0m\n", - "\u001b[33mFailed to authenticate '{'additional_properties': {}, 'id': '/tenants/d3e49573-1ecc-4a43-adfb-7400029d7049', 'tenant_id': 'd3e49573-1ecc-4a43-adfb-7400029d7049', 'tenant_category': 'Home', 'country': None, 'country_code': None, 'display_name': None, 'domains': None}' due to error 'Get Token request returned http error: 400 and server response: {\"error\":\"invalid_grant\",\"error_description\":\"AADSTS50020: User account '{EmailHidden}' from identity provider 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/' does not exist in tenant 'iconfitness.com' and cannot access the application '04b07795-8ddb-461a-bbee-02f9e1bf7b46'(Microsoft Azure CLI) in that tenant. The account needs to be added as an external user in the tenant first. Sign out and sign in again with a different Azure Active Directory user account.\\r\\nTrace ID: 0e29aa07-07fb-4843-a292-35074eda1d00\\r\\nCorrelation ID: cb2831fd-6b52-4bcb-bc90-d744682c896c\\r\\nTimestamp: 2022-12-06 22:22:42Z\",\"error_codes\":[50020],\"timestamp\":\"2022-12-06 22:22:42Z\",\"trace_id\":\"0e29aa07-07fb-4843-a292-35074eda1d00\",\"correlation_id\":\"cb2831fd-6b52-4bcb-bc90-d744682c896c\",\"error_uri\":\"https://login.microsoftonline.com/error?code=50020\"}'\u001b[0m\n", - "\u001b[33mThe following tenants don't contain accessible subscriptions. Use 'az login --allow-no-subscriptions' to have tenant level access.\u001b[0m\n", - "\u001b[33m985de3af-81be-4db4-b2e4-b6da729941fe 'Azure Global Critical Infrastructure'\u001b[0m\n", - "[\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"a1ffc958-d2c7-493e-9f1e-125a0477f536\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"MSFT-MVD-05-Shared-EUDB\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"2f96ae42-240b-4228-bafa-26d8b7b03bf3\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"MSFT-CloudMS-CPT-PRD-01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"8c40f547-9775-44d6-bb8b-19f20d050dd2\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"PlayFab.Analytics.Dev\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"94e8b8dd-5fb0-40f9-beb3-5b63eeb2aacc\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"PlayFab.PlayStream.NonProd\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"b10f7f41-91c1-4bdb-8d39-fe7feaa39d27\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"PlayFab.Analytics.Prod\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"e7348740-ed0e-4192-a663-dc7d353e311a\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"PlayFab.Insights.Essentials.Dev\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"5358a7bb-87ab-40e1-af58-c45f3657cdea\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"PlayFab.Insights.Dev\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"b5cb67a7-b716-42db-8ab5-203713124d5e\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"PlayFab.Stack.CICD.Stamp\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"7a1b56af-d64b-4525-b458-592a9f131a81\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"PlayFab.Stack.Dev.Partition\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"f5a7afd0-c585-4f46-b011-4024e1036293\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"GDX.PlayFab.Cerulean.Test.Dev\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"aad30b49-5326-4f4d-b7bd-8d408f5051e2\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"GDX.PlayFab.Cerulean.Test.Dev2\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"0ca618d2-22a8-413a-96d0-0f1b531129c3\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"Azure-Gaming-Private\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"6187b663-b744-4d24-8226-7e66525baf8f\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"AG-CI-AI-NI-SUPPLYCHAIN\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"2eedf122-c960-4ddc-9146-ac93dbf8b2b0\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"GAMING_PERSONALIZATION\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"95f31ea2-0e41-4d66-a5db-9ef0449ad928\",\n", - " \"isDefault\": true,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"AGS-GAME-TESTING\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"a11db6f9-92cb-4ef3-88ca-11c0d1e3789e\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.Community.Metrics.Prod03\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"b319454f-e9f9-4c54-be5c-d848e7a3d565\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.GXP.Metrics.Prod01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"8eccbb05-b393-4d06-9056-b9cd302590e6\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.GXP.Metrics.NonProd01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"d29ae919-0b7c-49d8-92a7-44cb3d3ff331\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.xGO.Metrics.Prod01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"ac6239b8-c012-498d-9241-37b92bdb091d\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.xGO.Metrics.NonProd01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"32fcca0f-6263-4450-b688-4737dc916f7d\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.GDX.Metrics.Prod01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"ea92ec9c-db3a-4d5d-8dea-cbc66e40d3aa\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.GDX.Metrics.NonProd01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"03aeb9f7-141e-4ec2-8921-1c6d4a69601f\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.Community.Metrics.Prod02\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"917d3e55-25a5-48ee-8a88-7c123c48444a\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.Community.Metrics.Prod01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"a05f3621-3dda-4ab6-8325-3e1e2dea1fe9\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.Community.Metrics.NonProd02\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"43a7747e-fad4-447d-a2e1-bacc1d19ff1d\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.Community.Metrics.NonProd01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"73ae2794-39e5-4e07-b2db-c256d950eaa7\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.Community.Kusto.Prod01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"387e5205-b3bb-42b2-83be-108fef1dd57a\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Gaming.Community.Kusto.NonProd01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"bac420ed-c6fc-4a05-8ac1-8c0c52da1d6e\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"IDEAs MS Reporting\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"dd4c2dac-db51-4cd0-b734-684c6cc360c1\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"Geneva Analytics Intelligent Data Test (Corp)\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"8c4b5b03-3b24-4ed0-91f5-a703cd91b412\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Cosmos_C&E_Azure_AzureEngineeringSystems_100200\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"79087098-5ecd-41bb-bc17-dd5c35aab810\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"AzPentest Sub 3\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"1278a874-89fc-418c-b6b9-ac763b000415\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"(NBrady) Microsoft Azure Internal Consumption\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"e686ef8c-d35d-4e9b-92f8-caaaa7948c0a\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Applied Sciences Group Dev\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"54b875cc-a81a-4914-8bfd-1a36bc7ddf4d\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"MSFT-WindowsVirtualDesktop-01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"c3547baf-332f-4d8f-96bd-0659b39c7a59\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"MSAI Search Research\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"407ff5d7-0113-4c5c-8534-f5cfb09298f5\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"msteams.nonprod.pub.msft.msts\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"588845a8-a4a7-4ab1-83a1-1388452e8c0c\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Saidi Dev and Test Subscription\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"dad45786-32e5-4ef3-b90e-8e0838fbadb6\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"AnE.ExP.Production\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"c60e2ea3-34e5-4667-95e4-d7da5166d8c8\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"ELITE_E360_PROD_R&D_01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"03bb6720-b3b0-45b0-9c37-748c34c86ad7\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"DS-SQL-CHOICE_HelsinkiLab_slavao_60843\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"21845c4f-ca8c-47bc-a3c5-12b1c15adccf\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"FIG Azure Services\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"09b5fdb3-165d-4e2b-8ca0-34f998d176d5\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"GameStreaming.Data.PROD.01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"b43a6159-1bea-4fa2-9407-e875fdc0ff55\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"BAP IA EU Schrems (P)\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"0f301386-8979-4981-acca-973d553078e7\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Scope Team Test Subscription\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"f30787b9-82a8-4e74-bb0f-f12d64ecc496\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Edge ExpInsights\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"5bcfd686-e65f-4381-94bd-44521888475c\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Epic-Edge-ES-MacLab-Dev\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"82ee6f14-d8a6-4395-97cd-4db8118bbe1b\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Epic-Edge-ES-MacLab-Prod\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"333e402a-65a0-45a9-8e23-867ca146c290\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Epic-Edge-ES-ReproVMLab\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"482e1993-01d4-4b16-bff4-1866929176a1\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Epic-Edge-ES-MergeResolver\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"26596251-f2f3-4e31-8a1b-f0754e32ad73\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Epic-Edge-ES-SrcPub-Prod\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"2876b824-a621-4dff-99b8-2fdb1a727b02\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"COSINE-ES-BS-DPB-BuildServices\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"e01de573-132a-42ac-9ee2-f9dea9dd2717\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Cosmos_WDG_Core_BnB_100292\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"4115b323-4aac-47f4-bb13-22af265ed58b\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"ES-ESP-AS-vPack-Nonprod\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"9ec1d932-0f3f-486c-acc6-e7d78b358f9b\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"TScience\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"8c2c7b23-848d-40fe-b817-690d79ad9dfd\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"Purview_Demo_PartnerSandbox\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"29de2cfc-f00a-43bb-bdc8-3108795bd282\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"MSFT-Modern Device-Modern Mgmt-Imaging00\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"7afc2d66-d5b4-4e84-970b-a782e3e4cc46\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"CLOUDBUILD-ANYBUILD-POC-01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"7d411d23-59e5-4e2e-8566-4f59de4544f2\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Dev:ER: CE Security Service Operation Center\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"0368444d-756e-4ca6-9ecd-e964248c227a\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"AnyBuild-InternalProdClusters\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"cb9eb375-570a-4e75-b83a-77dd942bee9f\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"PPP_ACBB_ATS_AKS_NonProd\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"009d0e9f-a42a-470e-b315-82496a88cf0f\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Weather and Finance INT\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"6f53185c-ea09-4fc3-9075-318dec805303\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"33e01921-4d64-4f8c-a055-5bdaffd5e33d\"\n", - " }\n", - " ],\n", - " \"name\": \"RPSAAS CSM Dogfood\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"dcbdac96-1896-478d-89fc-c95ed43f4596\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"pgattikoppul Projects\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"3b2fda06-3ef6-454a-9dd5-994a548243e9\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"Microsoft Employee & Executive Communications\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"227531a4-d775-435b-a878-963ed8d0d18f\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"cdc5aeea-15c5-4db6-b079-fcadd2505dc2\"\n", - " },\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"Skype-1ES-DEV\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"432a7068-99ae-4975-ad38-d96b71172cdf\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Azure Profiler - Testing\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"0cd6095b-b140-41ec-ad1d-32f2f7493386\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"1IS - One Integration Service - NonProduction\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"4628298e-882d-4f12-abf4-a9f9654960bb\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"ES_DeveloperProductivity_Dev\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"c0620f27-ac38-468c-a26b-264009fe7c41\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"AzureQualityInfrastructureSubscription-Test00\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"b68b2f37-1d37-4c2f-80f6-c23de402792e\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"Semantic Machines R&D Subscription 1\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"e4e06275-58d1-4081-8f1b-be12462eb701\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"CSR-CUSPOC-MST-Hiramfleitas\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"3cd95ff9-ac62-4b5c-8240-0cd046687ea0\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Internal App Insights Resources for Perflens\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"bbe41737-1ade-44df-8e33-217f11b8b452\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Service Profiler Development\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"1163fbbe-27e7-4b0f-8466-195fe5417043\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " },\n", - " {\n", - " \"tenantId\": \"d0ecd01b-d782-448e-bae0-c3cad0e0543a\"\n", - " },\n", - " {\n", - " \"tenantId\": \"94c4857e-1130-4ab8-8eac-069b40c9db20\"\n", - " },\n", - " {\n", - " \"tenantId\": \"f702a9dc-ae48-4dc7-8f0a-8155a6dfa4e5\"\n", - " }\n", - " ],\n", - " \"name\": \"Python_AI_Tools_PM_Team_Sub\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"8ae1955e-f748-4273-a507-10159ba940f9\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"SPO-CyrusB\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"9b4236fe-df75-4289-bf00-40628ed41fd9\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Customer Confidence Proof of Concepts\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"13723929-6644-4060-a50a-cc38ebc5e8b1\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"LinuxMdsd Test\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"10d0b7c6-9243-4713-91a9-2730375d3a1b\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"ASutton Subscription\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"3905431d-c062-4c17-8fd9-c51f89f334c4\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Pytorch Build\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"9372b318-ed3a-4504-95a6-941201300f78\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"OE-Service Management Platform - External - Prod\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"e0fd569c-e34a-4249-8c24-e8d723c7f054\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"AML - Responsible AI R&D\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"56207a5d-4746-4a2d-9f7f-c5ac77a5ddfd\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"Traffic manager Preview\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"bac044cf-49e1-4843-8dda-1ce9662606c8\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"UI Fabric\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"ca38bc19-cf50-48e2-bbe6-8c35b40212d8\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Maps Mining Azure\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"b2a328a7-ffff-4c09-b643-a4758cf170bc\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"AISC-DEV-02\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"85b3dbca-5974-4067-9669-67a141095a76\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"027fe226-784c-4798-a157-5e0e7893424d\"\n", - " }\n", - " ],\n", - " \"name\": \"Terraform Testing on Azure with TTL = 2 Days\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"8cff5d56-95fb-4a74-ab9d-079edb45313e\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"cdc5aeea-15c5-4db6-b079-fcadd2505dc2\"\n", - " }\n", - " ],\n", - " \"name\": \"Skype-NetEM-STAGING\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"68f3658f-0090-4277-a500-f02227aaee97\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"zejunz\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"79f57c16-00fe-48da-87d4-5192e86cd047\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"TScienceGPU\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"ae71ef11-a03f-4b4f-a0e6-ef144727c711\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"Bing MM Measurement\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"eec2de82-6ab2-4a84-ae5f-57e9a10bf661\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"ServicesPortfolio MCS\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"e72e5254-f265-4e95-9bd2-9ee8e7329051\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Speech Services - DEV - SDK (rob)\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"eef8b6d5-94da-4b36-9327-a662f2674efb\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"AISC-EngSys-01\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"ad203158-bc5d-4e72-b764-2607833a71dc\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"Project Vienna Build\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"d2c9544f-4329-4642-b73d-020e7fef844f\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"HPCScrub1\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"50ff7bc0-cd15-49d5-abb2-e975184c2f65\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"IDXXpertRandD Dev Data Catalog\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"4bbecc02-f2c3-402a-8e01-1dfb1ffef499\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Azure Portal Telemetry Reporting\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"5e552837-76cb-48b1-8983-b12ca284552b\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"jawelch\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"caffe3c0-acbd-4d01-af76-a45f421bfb64\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Azure_Base_LivesiteArmory_test\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"38d911df-2706-4195-8563-c75b49f7a88d\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Actions on AKS Dev\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"de6bc98d-2a4d-4607-919e-67692d1eba2b\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Dart Eng Beta\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"b8faf5eb-39a1-42c4-9e2a-335704d4c740\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"VSO-SD-POC\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"84ca48fe-c942-42e5-b492-d56681d058fa\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"AEP_CorePlatform_Playground_Dev\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"0ee78edb-a0ad-456c-a0a2-901bf542c102\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"ADF Test sub - App Model V2\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"8168a4f2-74d6-4663-9951-8e3a454937b7\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"TEST - IbizaFx misc\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"cd0fa82d-b6b6-4361-b002-050c32f71353\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Falcon Dev Cluster\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"b5c0b80f-5932-4d47-ae25-cd617dac90ce\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"CRM-DEVTEST-Efun-IDC\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"6560575d-fa06-4e7d-95fb-f962e74efd7a\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"ML Lifecycle PM\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"8d5565a3-dec1-4ee2-86d6-8aabb315eec4\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"Gandalf Analytics Service - Stage\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"d03b04c7-d1d4-467b-aaaa-87b6fcb38b38\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"VSEng Shared\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"bd62906c-0a81-43c3-a2f8-126e4cf66ada\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"DevDiv Key Vault\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"26b9b438-7fe8-482f-b732-ea99c70f2abb\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"ddverify\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"8bc2f89b-c4f6-4559-ad6a-4f2cfa6ccc49\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"VSEng MadDog-RPS Telemetry\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"eb87f285-893a-4f0f-8c55-7b4f67b1d097\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Dart Eng Alpha\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"58706c15-d8a1-4397-af3c-e1ebf1cbebe5\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"RPS-cloud-common-2\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"52a442a2-31e9-42f9-8e3e-4b27dbf82673\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"Core-ES-WorkManagement\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"365a62ee-6166-4d37-a936-03585106dd50\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"NetVal Dev Sandbox\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"a6c2a7cc-d67e-4a1a-b765-983f08c0423a\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " },\n", - " {\n", - " \"tenantId\": \"f40b18ba-b66c-49e4-9fd8-4fc7d3d19f0f\"\n", - " }\n", - " ],\n", - " \"name\": \"AI Customer Engineering - Internal Consumption\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"f8d440ed-264b-4a91-8ab9-0b1694914abf\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [],\n", - " \"name\": \"AGS_GamingProjects_Dev\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " },\n", - " {\n", - " \"cloudName\": \"AzureCloud\",\n", - " \"homeTenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"id\": \"c9126719-f58d-4455-a79c-6ddc2c326802\",\n", - " \"isDefault\": false,\n", - " \"managedByTenants\": [\n", - " {\n", - " \"tenantId\": \"2f4a9838-26b7-47ee-be60-ccc1fdec5953\"\n", - " }\n", - " ],\n", - " \"name\": \"ACE Customer Projects\",\n", - " \"state\": \"Enabled\",\n", - " \"tenantId\": \"72f988bf-86f1-41af-91ab-2d7cd011db47\",\n", - " \"user\": {\n", - " \"name\": \"jumin@microsoft.com\",\n", - " \"type\": \"user\"\n", - " }\n", - " }\n", - "]\n", - "\u001b[0m" - ] - } - ], + "outputs": [], "source": [ "if USE_CLI_AUTH:\n", " !az login --use-device-code" @@ -2374,7 +519,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 9, "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, @@ -2409,7 +554,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 10, "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, @@ -2480,7 +625,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 11, "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, @@ -2494,15 +639,15 @@ "name": "stderr", "output_type": "stream", "text": [ - "2022-12-06 12:39:40.576 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - secrets__azure_key_vault__name not found in the config file.\n", - "2022-12-06 12:39:40.600 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__s3__s3_enabled not found in the config file.\n", - "2022-12-06 12:39:40.619 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__jdbc__jdbc_enabled not found in the config file.\n", - "2022-12-06 12:39:40.624 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__snowflake__snowflake_enabled not found in the config file.\n", - "2022-12-06 12:39:40.634 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__feathr_runtime_location not found in the config file.\n", - "2022-12-06 12:39:40.640 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__workspace not found in the config file.\n", - "2022-12-06 12:39:40.644 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__master not found in the config file.\n", - "2022-12-06 12:39:40.653 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - feature_registry__purview__purview_name not found in the config file.\n", - "2022-12-06 12:39:40.655 | INFO | feathr.client:__init__:196 - Feathr client 0.9.0 initialized successfully.\n" + "2022-12-07 04:28:50.702 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - secrets__azure_key_vault__name not found in the config file.\n", + "2022-12-07 04:28:50.713 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__s3__s3_enabled not found in the config file.\n", + "2022-12-07 04:28:50.719 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__jdbc__jdbc_enabled not found in the config file.\n", + "2022-12-07 04:28:50.722 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__snowflake__snowflake_enabled not found in the config file.\n", + "2022-12-07 04:28:50.728 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__feathr_runtime_location not found in the config file.\n", + "2022-12-07 04:28:50.730 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__workspace not found in the config file.\n", + "2022-12-07 04:28:50.733 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__master not found in the config file.\n", + "2022-12-07 04:28:50.739 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - feature_registry__purview__purview_name not found in the config file.\n", + "2022-12-07 04:28:50.739 | INFO | feathr.client:__init__:196 - Feathr client 0.9.0 initialized successfully.\n" ] } ], @@ -2531,7 +676,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -2544,19 +689,26 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 13, "metadata": {}, "outputs": [ { - "ename": "NameError", - "evalue": "name 'PROJECT_NAME' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn [15], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[39m# upload dataset if needed\u001b[39;00m\n\u001b[0;32m----> 3\u001b[0m account_info_file_path \u001b[39m=\u001b[39m \u001b[39mstr\u001b[39m(Path(PROJECT_NAME, \u001b[39m\"\u001b[39m\u001b[39maccount_info.csv\u001b[39m\u001b[39m\"\u001b[39m))\n\u001b[1;32m 4\u001b[0m transactions_file_path \u001b[39m=\u001b[39m \u001b[39mstr\u001b[39m(Path(PROJECT_NAME, \u001b[39m\"\u001b[39m\u001b[39mtransactions.csv\u001b[39m\u001b[39m\"\u001b[39m))\n\u001b[1;32m 5\u001b[0m maybe_download(\n\u001b[1;32m 6\u001b[0m src_url\u001b[39m=\u001b[39mFRAUD_DETECTION_ACCOUNT_INFO_URL,\n\u001b[1;32m 7\u001b[0m dst_filepath\u001b[39m=\u001b[39maccount_info_file_path,\n\u001b[1;32m 8\u001b[0m )\n", - "\u001b[0;31mNameError\u001b[0m: name 'PROJECT_NAME' is not defined" + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 13.0k/13.0k [00:00<00:00, 16.3kKB/s]\n", + "100%|██████████| 786/786 [00:00<00:00, 2.34kKB/s]\n" ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -2574,6 +726,22 @@ ")" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Upload files to cloud\n", + "if client.spark_runtime == \"local\" or (client.spark_runtime == \"databricks\" and is_databricks()):\n", + " # In local mode, we can use the same data path as the source.\n", + " # If the notebook is running on databricks, DATA_FILE_PATH should be already a dbfs path.\n", + " data_source_path = DATA_FILE_PATH\n", + "else:\n", + " # Otherwise, upload the local file to the cloud storage (either dbfs or adls).\n", + " data_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(DATA_FILE_PATH) " + ] + }, { "cell_type": "markdown", "metadata": { @@ -3267,7 +1435,7 @@ "widgets": {} }, "kernelspec": { - "display_name": "Python 3.10.4 ('feathr')", + "display_name": "Python 3.10.8 ('feathr')", "language": "python", "name": "python3" }, @@ -3281,12 +1449,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.10.8" }, "orig_nbformat": 4, "vscode": { "interpreter": { - "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" + "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" } } }, diff --git a/docs/samples/nyc_taxi_demo.ipynb b/docs/samples/nyc_taxi_demo.ipynb index 78a6cfaec..fc4593e77 100644 --- a/docs/samples/nyc_taxi_demo.ipynb +++ b/docs/samples/nyc_taxi_demo.ipynb @@ -525,7 +525,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Define data source path\n", + "# Upload files to cloud\n", "if client.spark_runtime == \"local\" or (client.spark_runtime == \"databricks\" and is_databricks()):\n", " # In local mode, we can use the same data path as the source.\n", " # If the notebook is running on databricks, DATA_FILE_PATH should be already a dbfs path.\n", @@ -1122,7 +1122,7 @@ }, "celltoolbar": "Tags", "kernelspec": { - "display_name": "Python 3.10.4 ('feathr')", + "display_name": "Python 3.10.8 ('feathr')", "language": "python", "name": "python3" }, @@ -1136,11 +1136,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.10.8" }, "vscode": { "interpreter": { - "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" + "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" } } }, diff --git a/feathr_project/setup.py b/feathr_project/setup.py index 979a77e7a..85716733b 100644 --- a/feathr_project/setup.py +++ b/feathr_project/setup.py @@ -35,9 +35,8 @@ "pytest-mock>=3.8.1", ], notebook=[ - "azure-cli", # Azure CLI credentials - "jupyter==1.0.0", - "matplotlib==3.6.1", + "jupyter>=1.0.0", + "matplotlib>=3.6.1", "papermill>=2.1.2,<3", # to test run notebooks "scrapbook>=0.5.0,<1.0.0", # to scrap notebook outputs "scikit-learn", # for notebook examples From 83fc0ca43abd13039a5116df1c956e7e25e933ff Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Mon, 12 Dec 2022 19:10:00 -0800 Subject: [PATCH 03/22] Update fraud detection demo notebook and add test Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/fraud_detection_demo.ipynb | 2680 ++++++++--------- docs/samples/nyc_taxi_demo.ipynb | 14 +- feathr_project/feathr/client.py | 4 +- feathr_project/feathr/datasets/constants.py | 6 +- feathr_project/test/samples/test_notebooks.py | 29 + 5 files changed, 1261 insertions(+), 1472 deletions(-) diff --git a/docs/samples/fraud_detection_demo.ipynb b/docs/samples/fraud_detection_demo.ipynb index baa7e031d..ff344577d 100644 --- a/docs/samples/fraud_detection_demo.ipynb +++ b/docs/samples/fraud_detection_demo.ipynb @@ -1,1463 +1,1221 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "7b19a0cd-31da-45b7-91a4-9cd561f3d3d8", - "showTitle": false, - "title": "" - } - }, - "source": [ - "# Feathr Fraud Detection Sample\n", - "\n", - "This notebook illustrates the use of Feature Store to create a model that predicts the fraud status of transactions based on the user account data and trasaction data. All the data that was used in the notebook can be found here: https://github.com/microsoft/r-server-fraud-detection\n", - "\n", - "\n", - "In the following Notebook, we \n", - "1. Install the latest Feathr code (to include some unreleased features) \n", - "2. Define Environment Variables & `yaml_config` Settings \n", - "3. Create `FeathrClient` and Define `FeatureAnchor`\n", - "4. `build_features` and `get_offline_features` \n", - "5. Train Fraud Detection Model wih `KNeighborsClassifier`\n", - "6. `materialize_features` and `multi_get_online_features`\n", - "7. `register_features` and `list_registered_features`" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "0b51153e-40dd-43d5-9d3a-501534156e6d", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Setup Feathr Developer Environment" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "***Prior to running the notebook, if you have not deployed all the required resources, please refer to the guide here and follow the steps to do so: https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-arm.html***" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "b9c63dd5-304e-4797-a230-8fb753710dbc", - "showTitle": false, - "title": "" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Collecting feathr[notebook]\n", - " Cloning https://github.com/feathr-ai/feathr.git to /tmp/pip-install-zsn6kl5o/feathr_36df02ae02da42a0a56397c6e9e058e7\n", - " Running command git clone --filter=blob:none --quiet https://github.com/feathr-ai/feathr.git /tmp/pip-install-zsn6kl5o/feathr_36df02ae02da42a0a56397c6e9e058e7\n", - " Resolved https://github.com/feathr-ai/feathr.git to commit 3ebaa49cf36dac90005fb2cbf5412d9103871e9b\n", - " Installing build dependencies ... \u001b[?25ldone\n", - "\u001b[?25h Getting requirements to build wheel ... \u001b[?25ldone\n", - "\u001b[?25h Preparing metadata (pyproject.toml) ... \u001b[?25ldone\n", - "\u001b[?25hCollecting py4j<=0.10.9.7\n", - " Using cached py4j-0.10.9.7-py2.py3-none-any.whl (200 kB)\n", - "Collecting Jinja2<=3.1.2\n", - " Using cached Jinja2-3.1.2-py3-none-any.whl (133 kB)\n", - "Collecting pyspark>=3.1.2\n", - " Downloading pyspark-3.3.1.tar.gz (281.4 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m281.4/281.4 MB\u001b[0m \u001b[31m7.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25h Preparing metadata (setup.py) ... \u001b[?25ldone\n", - "\u001b[?25hCollecting confluent-kafka<=1.9.2\n", - " Using cached confluent_kafka-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.1 MB)\n", - "Collecting requests<=2.28.1\n", - " Using cached requests-2.28.1-py3-none-any.whl (62 kB)\n", - "Collecting azure-synapse-spark<=0.7.0\n", - " Using cached azure_synapse_spark-0.7.0-py2.py3-none-any.whl (32 kB)\n", - "Collecting databricks-cli<=0.17.3\n", - " Using cached databricks_cli-0.17.3-py3-none-any.whl\n", - "Collecting pyyaml<=6.0\n", - " Using cached PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (682 kB)\n", - "Collecting pyarrow<=9.0.0\n", - " Using cached pyarrow-9.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (35.3 MB)\n", - "Collecting pandas<=1.5.0\n", - " Using cached pandas-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (12.1 MB)\n", - "Collecting azure-storage-file-datalake<=12.5.0\n", - " Using cached azure_storage_file_datalake-12.5.0-py2.py3-none-any.whl (208 kB)\n", - "Collecting pandavro<=1.7.1\n", - " Using cached pandavro-1.7.1-py3-none-any.whl\n", - "Collecting tqdm<=4.64.1\n", - " Using cached tqdm-4.64.1-py2.py3-none-any.whl (78 kB)\n", - "Collecting redis<=4.4.0\n", - " Downloading redis-4.4.0-py3-none-any.whl (236 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m236.4/236.4 kB\u001b[0m \u001b[31m8.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting azure-identity>=1.8.0\n", - " Downloading azure_identity-1.12.0-py3-none-any.whl (135 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m135.5/135.5 kB\u001b[0m \u001b[31m11.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting azure-keyvault-secrets<=4.6.0\n", - " Using cached azure_keyvault_secrets-4.6.0-py3-none-any.whl (291 kB)\n", - "Collecting graphlib-backport<=1.0.3\n", - " Using cached graphlib_backport-1.0.3-py3-none-any.whl (5.1 kB)\n", - "Collecting click<=8.1.3\n", - " Using cached click-8.1.3-py3-none-any.whl (96 kB)\n", - "Collecting pyhocon<=0.3.59\n", - " Using cached pyhocon-0.3.59-py3-none-any.whl\n", - "Collecting msrest<=0.6.21\n", - " Using cached msrest-0.6.21-py2.py3-none-any.whl (85 kB)\n", - "Collecting azure-core<=1.22.1\n", - " Using cached azure_core-1.22.1-py3-none-any.whl (178 kB)\n", - "Collecting protobuf<=3.19.4,>=3.0.0\n", - " Downloading protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.1 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.1/1.1 MB\u001b[0m \u001b[31m13.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hCollecting loguru<=0.6.0\n", - " Using cached loguru-0.6.0-py3-none-any.whl (58 kB)\n", - "Collecting typing-extensions>=4.2.0\n", - " Downloading typing_extensions-4.4.0-py3-none-any.whl (26 kB)\n", - "Collecting deltalake>=0.6.2\n", - " Downloading deltalake-0.6.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (14.7 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m14.7/14.7 MB\u001b[0m \u001b[31m20.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hCollecting avro<=1.11.1\n", - " Using cached avro-1.11.1-py2.py3-none-any.whl\n", - "Collecting python-snappy<=0.6.1\n", - " Using cached python_snappy-0.6.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (55 kB)\n", - "Collecting pyapacheatlas<=0.14.0\n", - " Using cached pyapacheatlas-0.14.0-py3-none-any.whl (74 kB)\n", - "Collecting plotly\n", - " Downloading plotly-5.11.0-py2.py3-none-any.whl (15.3 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m15.3/15.3 MB\u001b[0m \u001b[31m20.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hCollecting jupyter==1.0.0\n", - " Using cached jupyter-1.0.0-py2.py3-none-any.whl (2.7 kB)\n", - "Collecting scrapbook<1.0.0,>=0.5.0\n", - " Using cached scrapbook-0.5.0-py3-none-any.whl (34 kB)\n", - "Collecting papermill<3,>=2.1.2\n", - " Using cached papermill-2.4.0-py3-none-any.whl (38 kB)\n", - "Collecting scikit-learn\n", - " Downloading scikit_learn-1.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (30.5 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m30.5/30.5 MB\u001b[0m \u001b[31m16.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hCollecting matplotlib==3.6.1\n", - " Using cached matplotlib-3.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.8 MB)\n", - "Collecting jupyter-console\n", - " Using cached jupyter_console-6.4.4-py3-none-any.whl (22 kB)\n", - "Collecting ipywidgets\n", - " Using cached ipywidgets-8.0.2-py3-none-any.whl (134 kB)\n", - "Requirement already satisfied: ipykernel in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from jupyter==1.0.0->feathr[notebook]) (6.17.1)\n", - "Collecting qtconsole\n", - " Downloading qtconsole-5.4.0-py3-none-any.whl (121 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m121.0/121.0 kB\u001b[0m \u001b[31m12.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting nbconvert\n", - " Downloading nbconvert-7.2.6-py3-none-any.whl (273 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m273.2/273.2 kB\u001b[0m \u001b[31m14.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting notebook\n", - " Downloading notebook-6.5.2-py3-none-any.whl (439 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m439.1/439.1 kB\u001b[0m \u001b[31m21.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting cycler>=0.10\n", - " Using cached cycler-0.11.0-py3-none-any.whl (6.4 kB)\n", - "Requirement already satisfied: packaging>=20.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from matplotlib==3.6.1->feathr[notebook]) (21.3)\n", - "Collecting fonttools>=4.22.0\n", - " Downloading fonttools-4.38.0-py3-none-any.whl (965 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m965.4/965.4 kB\u001b[0m \u001b[31m15.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hRequirement already satisfied: python-dateutil>=2.7 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from matplotlib==3.6.1->feathr[notebook]) (2.8.2)\n", - "Collecting pillow>=6.2.0\n", - " Downloading Pillow-9.3.0-cp310-cp310-manylinux_2_28_x86_64.whl (3.3 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m3.3/3.3 MB\u001b[0m \u001b[31m7.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0mm\n", - "\u001b[?25hCollecting kiwisolver>=1.0.1\n", - " Using cached kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.6 MB)\n", - "Requirement already satisfied: pyparsing>=2.2.1 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from matplotlib==3.6.1->feathr[notebook]) (3.0.9)\n", - "Collecting numpy>=1.19\n", - " Downloading numpy-1.23.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (17.1 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m17.1/17.1 MB\u001b[0m \u001b[31m12.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hCollecting contourpy>=1.0.1\n", - " Downloading contourpy-1.0.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (296 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m296.1/296.1 kB\u001b[0m \u001b[31m16.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hRequirement already satisfied: six>=1.11.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from azure-core<=1.22.1->feathr[notebook]) (1.16.0)\n", - "Collecting cryptography>=2.5\n", - " Downloading cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl (4.2 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m4.2/4.2 MB\u001b[0m \u001b[31m17.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hCollecting msal-extensions<2.0.0,>=0.3.0\n", - " Using cached msal_extensions-1.0.0-py2.py3-none-any.whl (19 kB)\n", - "Collecting msal<2.0.0,>=1.12.0\n", - " Downloading msal-1.20.0-py2.py3-none-any.whl (90 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m90.0/90.0 kB\u001b[0m \u001b[31m7.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting azure-common~=1.1\n", - " Using cached azure_common-1.1.28-py2.py3-none-any.whl (14 kB)\n", - "Collecting azure-storage-blob<13.0.0,>=12.9.0b1\n", - " Downloading azure_storage_blob-12.14.1-py3-none-any.whl (383 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m383.2/383.2 kB\u001b[0m \u001b[31m1.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hCollecting oauthlib>=3.1.0\n", - " Downloading oauthlib-3.2.2-py3-none-any.whl (151 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m151.7/151.7 kB\u001b[0m \u001b[31m9.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting pyjwt>=1.7.0\n", - " Downloading PyJWT-2.6.0-py3-none-any.whl (20 kB)\n", - "Collecting tabulate>=0.7.7\n", - " Downloading tabulate-0.9.0-py3-none-any.whl (35 kB)\n", - "Collecting MarkupSafe>=2.0\n", - " Using cached MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (25 kB)\n", - "Collecting requests-oauthlib>=0.5.0\n", - " Using cached requests_oauthlib-1.3.1-py2.py3-none-any.whl (23 kB)\n", - "Collecting isodate>=0.6.0\n", - " Using cached isodate-0.6.1-py2.py3-none-any.whl (41 kB)\n", - "Requirement already satisfied: certifi>=2017.4.17 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from msrest<=0.6.21->feathr[notebook]) (2022.9.24)\n", - "Collecting pytz>=2020.1\n", - " Downloading pytz-2022.6-py2.py3-none-any.whl (498 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m498.1/498.1 kB\u001b[0m \u001b[31m17.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting fastavro==1.5.1\n", - " Using cached fastavro-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.5 MB)\n", - "Collecting tenacity\n", - " Using cached tenacity-8.1.0-py3-none-any.whl (23 kB)\n", - "Requirement already satisfied: entrypoints in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from papermill<3,>=2.1.2->feathr[notebook]) (0.4)\n", - "Collecting nbformat>=5.1.2\n", - " Using cached nbformat-5.7.0-py3-none-any.whl (77 kB)\n", - "Collecting nbclient>=0.2.0\n", - " Downloading nbclient-0.7.2-py3-none-any.whl (71 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m72.0/72.0 kB\u001b[0m \u001b[31m4.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting ansiwrap\n", - " Using cached ansiwrap-0.8.4-py2.py3-none-any.whl (8.5 kB)\n", - "Collecting openpyxl>=3.0\n", - " Using cached openpyxl-3.0.10-py2.py3-none-any.whl (242 kB)\n", - "Collecting pyparsing>=2.2.1\n", - " Using cached pyparsing-2.4.7-py2.py3-none-any.whl (67 kB)\n", - "Collecting py4j<=0.10.9.7\n", - " Using cached py4j-0.10.9.5-py2.py3-none-any.whl (199 kB)\n", - "Collecting async-timeout>=4.0.2\n", - " Using cached async_timeout-4.0.2-py3-none-any.whl (5.8 kB)\n", - "Collecting charset-normalizer<3,>=2\n", - " Using cached charset_normalizer-2.1.1-py3-none-any.whl (39 kB)\n", - "Collecting urllib3<1.27,>=1.21.1\n", - " Downloading urllib3-1.26.13-py2.py3-none-any.whl (140 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m140.6/140.6 kB\u001b[0m \u001b[31m7.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting idna<4,>=2.5\n", - " Using cached idna-3.4-py3-none-any.whl (61 kB)\n", - "Requirement already satisfied: ipython in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (8.7.0)\n", - "Collecting jsonschema\n", - " Downloading jsonschema-4.17.3-py3-none-any.whl (90 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m90.4/90.4 kB\u001b[0m \u001b[31m8.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting threadpoolctl>=2.0.0\n", - " Downloading threadpoolctl-3.1.0-py3-none-any.whl (14 kB)\n", - "Collecting joblib>=1.0.0\n", - " Downloading joblib-1.2.0-py3-none-any.whl (297 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m298.0/298.0 kB\u001b[0m \u001b[31m4.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hCollecting scipy>=1.3.2\n", - " Downloading scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (33.7 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m33.7/33.7 MB\u001b[0m \u001b[31m16.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hCollecting azure-storage-blob<13.0.0,>=12.9.0b1\n", - " Downloading azure_storage_blob-12.14.0-py3-none-any.whl (383 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m383.2/383.2 kB\u001b[0m \u001b[31m14.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25h Using cached azure_storage_blob-12.14.0b2-py3-none-any.whl (383 kB)\n", - " Using cached azure_storage_blob-12.14.0b1-py3-none-any.whl (381 kB)\n", - " Using cached azure_storage_blob-12.13.1-py3-none-any.whl (377 kB)\n", - " Using cached azure_storage_blob-12.13.0-py3-none-any.whl (377 kB)\n", - " Using cached azure_storage_blob-12.13.0b1-py3-none-any.whl (377 kB)\n", - " Using cached azure_storage_blob-12.12.0-py3-none-any.whl (366 kB)\n", - " Using cached azure_storage_blob-12.12.0b1-py3-none-any.whl (365 kB)\n", - " Using cached azure_storage_blob-12.11.0-py3-none-any.whl (346 kB)\n", - "Collecting cffi>=1.12\n", - " Using cached cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (441 kB)\n", - "Collecting portalocker<3,>=1.0\n", - " Downloading portalocker-2.6.0-py2.py3-none-any.whl (15 kB)\n", - "Requirement already satisfied: jupyter-core!=5.0.*,>=4.12 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from nbclient>=0.2.0->papermill<3,>=2.1.2->feathr[notebook]) (5.1.0)\n", - "Requirement already satisfied: jupyter-client>=6.1.12 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from nbclient>=0.2.0->papermill<3,>=2.1.2->feathr[notebook]) (7.4.8)\n", - "Requirement already satisfied: traitlets>=5.3 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from nbclient>=0.2.0->papermill<3,>=2.1.2->feathr[notebook]) (5.6.0)\n", - "Collecting fastjsonschema\n", - " Using cached fastjsonschema-2.16.2-py3-none-any.whl (22 kB)\n", - "Collecting attrs>=17.4.0\n", - " Using cached attrs-22.1.0-py2.py3-none-any.whl (58 kB)\n", - "Collecting pyrsistent!=0.17.0,!=0.17.1,!=0.17.2,>=0.14.0\n", - " Downloading pyrsistent-0.19.2-py3-none-any.whl (57 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m57.5/57.5 kB\u001b[0m \u001b[31m6.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting et-xmlfile\n", - " Using cached et_xmlfile-1.1.0-py3-none-any.whl (4.7 kB)\n", - "Collecting textwrap3>=0.9.2\n", - " Using cached textwrap3-0.9.2-py2.py3-none-any.whl (12 kB)\n", - "Requirement already satisfied: tornado>=6.1 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (6.2)\n", - "Requirement already satisfied: nest-asyncio in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (1.5.6)\n", - "Requirement already satisfied: matplotlib-inline>=0.1 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (0.1.6)\n", - "Requirement already satisfied: psutil in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (5.9.4)\n", - "Requirement already satisfied: pyzmq>=17 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (24.0.1)\n", - "Requirement already satisfied: debugpy>=1.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipykernel->jupyter==1.0.0->feathr[notebook]) (1.6.4)\n", - "Requirement already satisfied: pickleshare in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.7.5)\n", - "Requirement already satisfied: decorator in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (5.1.1)\n", - "Requirement already satisfied: pexpect>4.3 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (4.8.0)\n", - "Requirement already satisfied: jedi>=0.16 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.18.2)\n", - "Requirement already satisfied: stack-data in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.6.2)\n", - "Requirement already satisfied: backcall in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.2.0)\n", - "Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.11 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (3.0.33)\n", - "Requirement already satisfied: pygments>=2.4.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (2.13.0)\n", - "Collecting jupyterlab-widgets~=3.0\n", - " Using cached jupyterlab_widgets-3.0.3-py3-none-any.whl (384 kB)\n", - "Collecting widgetsnbextension~=4.0\n", - " Using cached widgetsnbextension-4.0.3-py3-none-any.whl (2.0 MB)\n", - "Collecting beautifulsoup4\n", - " Using cached beautifulsoup4-4.11.1-py3-none-any.whl (128 kB)\n", - "Collecting bleach\n", - " Using cached bleach-5.0.1-py3-none-any.whl (160 kB)\n", - "Collecting pandocfilters>=1.4.1\n", - " Using cached pandocfilters-1.5.0-py2.py3-none-any.whl (8.7 kB)\n", - "Collecting defusedxml\n", - " Using cached defusedxml-0.7.1-py2.py3-none-any.whl (25 kB)\n", - "Collecting mistune<3,>=2.0.3\n", - " Using cached mistune-2.0.4-py2.py3-none-any.whl (24 kB)\n", - "Collecting tinycss2\n", - " Using cached tinycss2-1.2.1-py3-none-any.whl (21 kB)\n", - "Collecting jupyterlab-pygments\n", - " Using cached jupyterlab_pygments-0.2.2-py2.py3-none-any.whl (21 kB)\n", - "Collecting terminado>=0.8.3\n", - " Downloading terminado-0.17.1-py3-none-any.whl (17 kB)\n", - "Collecting argon2-cffi\n", - " Using cached argon2_cffi-21.3.0-py3-none-any.whl (14 kB)\n", - "Collecting nbclassic>=0.4.7\n", - " Downloading nbclassic-0.4.8-py3-none-any.whl (9.8 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m9.8/9.8 MB\u001b[0m \u001b[31m14.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hCollecting prometheus-client\n", - " Using cached prometheus_client-0.15.0-py3-none-any.whl (60 kB)\n", - "Collecting Send2Trash>=1.8.0\n", - " Using cached Send2Trash-1.8.0-py3-none-any.whl (18 kB)\n", - "Collecting ipython-genutils\n", - " Using cached ipython_genutils-0.2.0-py2.py3-none-any.whl (26 kB)\n", - "Collecting qtpy>=2.0.1\n", - " Downloading QtPy-2.3.0-py3-none-any.whl (83 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m83.6/83.6 kB\u001b[0m \u001b[31m7.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting pycparser\n", - " Using cached pycparser-2.21-py2.py3-none-any.whl (118 kB)\n", - "Requirement already satisfied: parso<0.9.0,>=0.8.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from jedi>=0.16->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.8.3)\n", - "Requirement already satisfied: platformdirs>=2.5 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from jupyter-core!=5.0.*,>=4.12->nbclient>=0.2.0->papermill<3,>=2.1.2->feathr[notebook]) (2.5.4)\n", - "Collecting notebook-shim>=0.1.0\n", - " Downloading notebook_shim-0.2.2-py3-none-any.whl (13 kB)\n", - "Collecting jupyter-server>=1.8\n", - " Downloading jupyter_server-1.23.3-py3-none-any.whl (346 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m346.4/346.4 kB\u001b[0m \u001b[31m15.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hRequirement already satisfied: ptyprocess>=0.5 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from pexpect>4.3->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.7.0)\n", - "Requirement already satisfied: wcwidth in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from prompt-toolkit<3.1.0,>=3.0.11->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.2.5)\n", - "Collecting argon2-cffi-bindings\n", - " Using cached argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (86 kB)\n", - "Collecting soupsieve>1.2\n", - " Using cached soupsieve-2.3.2.post1-py3-none-any.whl (37 kB)\n", - "Collecting webencodings\n", - " Using cached webencodings-0.5.1-py2.py3-none-any.whl (11 kB)\n", - "Requirement already satisfied: asttokens>=2.1.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from stack-data->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (2.2.1)\n", - "Requirement already satisfied: pure-eval in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from stack-data->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (0.2.2)\n", - "Requirement already satisfied: executing>=1.2.0 in /home/jumin/miniconda3/envs/feathr_main/lib/python3.10/site-packages (from stack-data->ipython->scrapbook<1.0.0,>=0.5.0->feathr[notebook]) (1.2.0)\n", - "Collecting websocket-client\n", - " Downloading websocket_client-1.4.2-py3-none-any.whl (55 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m55.3/55.3 kB\u001b[0m \u001b[31m5.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting anyio<4,>=3.1.0\n", - " Using cached anyio-3.6.2-py3-none-any.whl (80 kB)\n", - "Collecting sniffio>=1.1\n", - " Using cached sniffio-1.3.0-py3-none-any.whl (10 kB)\n", - "Building wheels for collected packages: pyspark, feathr\n", - " Building wheel for pyspark (setup.py) ... \u001b[?25ldone\n", - "\u001b[?25h Created wheel for pyspark: filename=pyspark-3.3.1-py2.py3-none-any.whl size=281845499 sha256=0f475103c6dfe6a385bf18effc5fa0fca67de759aab1f4250f202173b4b007b9\n", - " Stored in directory: /home/jumin/.cache/pip/wheels/0f/f0/3d/517368b8ce80486e84f89f214e0a022554e4ee64969f46279b\n", - " Building wheel for feathr (pyproject.toml) ... \u001b[?25ldone\n", - "\u001b[?25h Created wheel for feathr: filename=feathr-0.9.0-py3-none-any.whl size=119311 sha256=4e413243162d4ab56e4de61da2580a86fa761a76f836df8e8b9084bcf4bf0df8\n", - " Stored in directory: /tmp/pip-ephem-wheel-cache-3fzfmubz/wheels/72/6a/79/78f71da32c2bdf8534846eb5255ec37a860842063d636f9193\n", - "Successfully built pyspark feathr\n", - "Installing collected packages: webencodings, textwrap3, Send2Trash, pytz, python-snappy, py4j, mistune, ipython-genutils, fastjsonschema, confluent-kafka, azure-common, widgetsnbextension, websocket-client, urllib3, typing-extensions, tqdm, tinycss2, threadpoolctl, terminado, tenacity, tabulate, soupsieve, sniffio, pyyaml, pyspark, pyrsistent, pyparsing, pyjwt, pycparser, protobuf, prometheus-client, portalocker, pillow, pandocfilters, oauthlib, numpy, MarkupSafe, loguru, kiwisolver, jupyterlab-widgets, jupyterlab-pygments, joblib, isodate, idna, graphlib-backport, fonttools, fastavro, et-xmlfile, defusedxml, cycler, click, charset-normalizer, bleach, avro, attrs, async-timeout, ansiwrap, scipy, requests, redis, pyhocon, pyarrow, plotly, pandas, openpyxl, jsonschema, Jinja2, contourpy, cffi, beautifulsoup4, anyio, scikit-learn, requests-oauthlib, qtpy, pyapacheatlas, pandavro, nbformat, matplotlib, deltalake, databricks-cli, cryptography, azure-core, argon2-cffi-bindings, nbclient, msrest, argon2-cffi, qtconsole, papermill, nbconvert, msal, jupyter-console, ipywidgets, azure-synapse-spark, azure-storage-blob, azure-keyvault-secrets, scrapbook, msal-extensions, jupyter-server, azure-storage-file-datalake, notebook-shim, azure-identity, nbclassic, feathr, notebook, jupyter\n", - " Attempting uninstall: pyparsing\n", - " Found existing installation: pyparsing 3.0.9\n", - " Uninstalling pyparsing-3.0.9:\n", - " Successfully uninstalled pyparsing-3.0.9\n", - "Successfully installed Jinja2-3.1.2 MarkupSafe-2.1.1 Send2Trash-1.8.0 ansiwrap-0.8.4 anyio-3.6.2 argon2-cffi-21.3.0 argon2-cffi-bindings-21.2.0 async-timeout-4.0.2 attrs-22.1.0 avro-1.11.1 azure-common-1.1.28 azure-core-1.22.1 azure-identity-1.12.0 azure-keyvault-secrets-4.6.0 azure-storage-blob-12.11.0 azure-storage-file-datalake-12.5.0 azure-synapse-spark-0.7.0 beautifulsoup4-4.11.1 bleach-5.0.1 cffi-1.15.1 charset-normalizer-2.1.1 click-8.1.3 confluent-kafka-1.9.2 contourpy-1.0.6 cryptography-38.0.4 cycler-0.11.0 databricks-cli-0.17.3 defusedxml-0.7.1 deltalake-0.6.4 et-xmlfile-1.1.0 fastavro-1.5.1 fastjsonschema-2.16.2 feathr-0.9.0 fonttools-4.38.0 graphlib-backport-1.0.3 idna-3.4 ipython-genutils-0.2.0 ipywidgets-8.0.2 isodate-0.6.1 joblib-1.2.0 jsonschema-4.17.3 jupyter-1.0.0 jupyter-console-6.4.4 jupyter-server-1.23.3 jupyterlab-pygments-0.2.2 jupyterlab-widgets-3.0.3 kiwisolver-1.4.4 loguru-0.6.0 matplotlib-3.6.1 mistune-2.0.4 msal-1.20.0 msal-extensions-1.0.0 msrest-0.6.21 nbclassic-0.4.8 nbclient-0.7.2 nbconvert-7.2.6 nbformat-5.7.0 notebook-6.5.2 notebook-shim-0.2.2 numpy-1.23.5 oauthlib-3.2.2 openpyxl-3.0.10 pandas-1.5.0 pandavro-1.7.1 pandocfilters-1.5.0 papermill-2.4.0 pillow-9.3.0 plotly-5.11.0 portalocker-2.6.0 prometheus-client-0.15.0 protobuf-3.19.4 py4j-0.10.9.5 pyapacheatlas-0.14.0 pyarrow-9.0.0 pycparser-2.21 pyhocon-0.3.59 pyjwt-2.6.0 pyparsing-2.4.7 pyrsistent-0.19.2 pyspark-3.3.1 python-snappy-0.6.1 pytz-2022.6 pyyaml-6.0 qtconsole-5.4.0 qtpy-2.3.0 redis-4.4.0 requests-2.28.1 requests-oauthlib-1.3.1 scikit-learn-1.1.3 scipy-1.9.3 scrapbook-0.5.0 sniffio-1.3.0 soupsieve-2.3.2.post1 tabulate-0.9.0 tenacity-8.1.0 terminado-0.17.1 textwrap3-0.9.2 threadpoolctl-3.1.0 tinycss2-1.2.1 tqdm-4.64.1 typing-extensions-4.4.0 urllib3-1.26.13 webencodings-0.5.1 websocket-client-1.4.2 widgetsnbextension-4.0.3\n" - ] - } - ], - "source": [ - "# Install feathr from the latest codes in the repo. You may use `pip install feathr[notebook]` as well.\n", - "!pip install \"git+https://github.com/feathr-ai/feathr.git#subdirectory=feathr_project&egg=feathr[notebook]\" " - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "69222adf-1cb0-410b-b98d-e22877f358c0", - "showTitle": false, - "title": "" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Feathr version: 0.9.0\n" - ] - } - ], - "source": [ - "from datetime import datetime, timedelta\n", - "import glob\n", - "from math import sqrt\n", - "import os\n", - "import tempfile\n", - "\n", - "from azure.identity import DefaultAzureCredential\n", - "from azure.keyvault.secrets import SecretClient\n", - "from sklearn.model_selection import train_test_split\n", - "\n", - "import feathr\n", - "from feathr import (\n", - " FeathrClient,\n", - " STRING, BOOLEAN, FLOAT, INT32, ValueType,\n", - " Feature, DerivedFeature, FeatureAnchor,\n", - " BackfillTime, MaterializationSettings,\n", - " FeatureQuery, ObservationSettings,\n", - " RedisSink,\n", - " HdfsSource,\n", - " WindowAggTransformation,\n", - " TypedKey,\n", - ")\n", - "from feathr.utils.config import generate_config\n", - "\n", - "\n", - "print(f\"Feathr version: {feathr.__version__}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "tags": [ - "parameters" - ] - }, - "outputs": [], - "source": [ - "RESOURCE_PREFIX = None # TODO fill the value used to deploy the resources via ARM template\n", - "PROJECT_NAME = \"fraud_detection\"\n", - "\n", - "# Currently support: 'azure_synapse', 'databricks', and 'local' \n", - "SPARK_CLUSTER = \"local\"\n", - "\n", - "# TODO fill values to use databricks cluster:\n", - "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", - "DATABRICKS_URL = None # Set Databricks workspace url to use databricks\n", - "DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", - "\n", - "# TODO fill values to use Azure Synapse cluster:\n", - "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", - "AZURE_SYNAPSE_URL = None # Set Azure Synapse workspace url to use Azure Synapse\n", - "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", - "\n", - "# An existing Feathr config file path. If None, we'll generate a new config based on the constants in this cell.\n", - "FEATHR_CONFIG_PATH = None\n", - "\n", - "# If set True, use an interactive browser authentication to get the redis password.\n", - "USE_CLI_AUTH = False" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "# TODO remove this cell\n", - "RESOURCE_PREFIX = \"juntest\"\n", - "USE_CLI_AUTH = False" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "if SPARK_CLUSTER == \"azure_synapse\" and not os.environ.get(\"ADLS_KEY\"):\n", - " os.environ[\"ADLS_KEY\"] = ADLS_KEY\n", - "elif SPARK_CLUSTER == \"databricks\" and not os.environ.get(\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"):\n", - " os.environ[\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"] = DATABRICKS_WORKSPACE_TOKEN_VALUE" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "c0299d67-1103-4aa4-ba57-300498ae2579", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "if USE_CLI_AUTH:\n", - " !az login --use-device-code" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Permission\n", - "To run the cells below, you need additional permission: permission to your managed identity to access the keyvault, and permission to the user to access the Storage Blob. Run the following lines of command in the Cloud Shell in order to grant yourself the access.\n", - "\n", - "```\n", - "userId=\n", - "resource_prefix=\n", - "synapse_workspace_name=\"${resource_prefix}syws\"\n", - "keyvault_name=\"${resource_prefix}kv\"\n", - "objectId=$(az ad user show --id $userId --query id -o tsv)\n", - "az keyvault update --name $keyvault_name --enable-rbac-authorization false\n", - "az keyvault set-policy -n $keyvault_name --secret-permissions get list --object-id $objectId\n", - "az role assignment create --assignee $userId --role \"Storage Blob Data Contributor\"\n", - "az synapse role assignment create --workspace-name $synapse_workspace_name --role \"Synapse Contributor\" --assignee $userId\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "a8a70f27-d520-4d3c-bb8c-f364f84cb738", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "# Redis password\n", - "if 'REDIS_PASSWORD' not in os.environ:\n", - " # Try to get all the required credentials from Azure Key Vault\n", - " from azure.identity import AzureCliCredential, DefaultAzureCredential \n", - " from azure.keyvault.secrets import SecretClient\n", - "\n", - " # TODO assume the resources are deployed by using the ARM template. If not, please set your vault url name.\n", - " vault_url = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", - " if USE_CLI_AUTH:\n", - " credential = AzureCliCredential(additionally_allowed_tenants=['*'],)\n", - " else:\n", - " credential = DefaultAzureCredential(\n", - " exclude_interactive_browser_credential=False,\n", - " additionally_allowed_tenants=['*'],\n", - " )\n", - " secret_client = SecretClient(vault_url=vault_url, credential=credential)\n", - " retrieved_secret = secret_client.get_secret('FEATHR-ONLINE-STORE-CONN').value\n", - " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]\n", - "\n", - "# feathr_output_path = f'abfss://{adls_fs_name}@{adls_account}.dfs.core.windows.net/feathr_output'" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "50b2f73e-6380-42c3-91e8-4f3e15bc10d6", - "showTitle": false, - "title": "" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "api_version: 1\n", - "feature_registry:\n", - " api_endpoint: https://juntestwebapp.azurewebsites.net/api/v1\n", - "offline_store:\n", - " adls:\n", - " adls_enabled: 'true'\n", - " wasb:\n", - " wasb_enabled: 'true'\n", - "online_store:\n", - " redis:\n", - " host: juntestredis.redis.cache.windows.net\n", - " port: '6380'\n", - " ssl_enabled: 'true'\n", - "project_config:\n", - " project_name: fraud_detection\n", - "spark_config:\n", - " spark_cluster: local\n", - " spark_result_output_parts: '1'\n", - "\n" - ] - } - ], - "source": [ - "if FEATHR_CONFIG_PATH:\n", - " config_path = FEATHR_CONFIG_PATH\n", - "else:\n", - " config_path = generate_config(\n", - " resource_prefix=RESOURCE_PREFIX,\n", - " project_name=PROJECT_NAME,\n", - " spark_config__spark_cluster=SPARK_CLUSTER,\n", - " spark_config__azure_synapse__dev_url=AZURE_SYNAPSE_URL,\n", - " spark_config__azure_synapse__pool_name=AZURE_SYNAPSE_SPARK_POOL,\n", - " spark_config__databricks__workspace_instance_url=DATABRICKS_URL,\n", - " databricks_cluster_id=DATABRICKS_CLUSTER_ID,\n", - " )\n", - "\n", - "with open(config_path, 'r') as f: \n", - " print(f.read())" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "eab0957c-c906-4297-a729-8dd8d79cb629", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Initialize `Feathr Client`\n", - "- `FeathrClient`" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "3734eee3-12f9-44db-a440-ad375ef859f0", - "showTitle": false, - "title": "" - } - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2022-12-07 04:28:50.702 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - secrets__azure_key_vault__name not found in the config file.\n", - "2022-12-07 04:28:50.713 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__s3__s3_enabled not found in the config file.\n", - "2022-12-07 04:28:50.719 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__jdbc__jdbc_enabled not found in the config file.\n", - "2022-12-07 04:28:50.722 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - offline_store__snowflake__snowflake_enabled not found in the config file.\n", - "2022-12-07 04:28:50.728 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__feathr_runtime_location not found in the config file.\n", - "2022-12-07 04:28:50.730 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__workspace not found in the config file.\n", - "2022-12-07 04:28:50.733 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - spark_config__local__master not found in the config file.\n", - "2022-12-07 04:28:50.739 | INFO | feathr.utils._envvariableutil:get_environment_variable_with_default:51 - feature_registry__purview__purview_name not found in the config file.\n", - "2022-12-07 04:28:50.739 | INFO | feathr.client:__init__:196 - Feathr client 0.9.0 initialized successfully.\n" - ] - } - ], - "source": [ - "client = FeathrClient(config_path=config_path, credential=credential)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "f6adbca1-5642-4ac1-bff7-e7c9d4d9e5b2", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Define Features\n", - "- `HdfsSource`\n", - "- `TypedKey`\n", - "- `Feature`\n", - "- `FeatureAnchor`\n", - "- `DerivedFeature`" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "from feathr.datasets.constants import FRAUD_DETECTION_ACCOUNT_INFO_URL, FRAUD_DETECTION_TRANSACTIONS_URL\n", - "\n", - "from feathr.datasets.utils import maybe_download\n", - "\n", - "from pathlib import Path" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 13.0k/13.0k [00:00<00:00, 16.3kKB/s]\n", - "100%|██████████| 786/786 [00:00<00:00, 2.34kKB/s]\n" - ] - }, - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# upload dataset if needed\n", - "\n", - "account_info_file_path = str(Path(PROJECT_NAME, \"account_info.csv\"))\n", - "transactions_file_path = str(Path(PROJECT_NAME, \"transactions.csv\"))\n", - "maybe_download(\n", - " src_url=FRAUD_DETECTION_ACCOUNT_INFO_URL,\n", - " dst_filepath=account_info_file_path,\n", - ")\n", - "maybe_download(\n", - " src_url=FRAUD_DETECTION_TRANSACTIONS_URL,\n", - " dst_filepath=transactions_file_path,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Upload files to cloud\n", - "if client.spark_runtime == \"local\" or (client.spark_runtime == \"databricks\" and is_databricks()):\n", - " # In local mode, we can use the same data path as the source.\n", - " # If the notebook is running on databricks, DATA_FILE_PATH should be already a dbfs path.\n", - " data_source_path = DATA_FILE_PATH\n", - "else:\n", - " # Otherwise, upload the local file to the cloud storage (either dbfs or adls).\n", - " data_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(DATA_FILE_PATH) " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "b073b509-0f95-4e23-b16b-ffd8190fb6a2", - "showTitle": false, - "title": "" - } - }, - "source": [ - "### Account Features" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "b3668eeb-e4a0-4327-baf6-5521c856f51d", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "#Refer to to learn more about the details of each method\n", - "account_info = HdfsSource(\n", - " name=\"AccountData\",\n", - " path=\"wasbs://frauddata@feathrdatastorage.blob.core.windows.net/account_out_small.csv\",\n", - " event_timestamp_column=\"transactionDate\",\n", - " timestamp_format=\"yyyyMMdd\",\n", - ")\n", - "\n", - "accountId = TypedKey(key_column=\"accountID\",\n", - " key_column_type=ValueType.INT32,\n", - " description=\"account id\")\n", - "\n", - "account_country = Feature(name=\"account_country\",\n", - " key=accountId,\n", - " feature_type=STRING, \n", - " transform=\"accountCountry\")\n", - "\n", - "is_user_registered = Feature(name=\"is_user_registered\",\n", - " key=accountId,\n", - " feature_type=BOOLEAN,\n", - " transform=\"isUserRegistered==TRUE\")\n", - "\n", - "num_payment_rejects_1d_per_user = Feature(name=\"num_payment_rejects_1d_per_user\",\n", - " key=accountId,\n", - " feature_type=INT32,\n", - " transform=\"numPaymentRejects1dPerUser\")\n", - "\n", - "account_age = Feature(name=\"account_age\",\n", - " key=accountId,\n", - " feature_type=INT32,\n", - " transform=\"accountAge\")\n", - " \n", - "features = [\n", - " account_country,\n", - " account_age,\n", - " is_user_registered,\n", - " num_payment_rejects_1d_per_user\n", - "]\n", - "\n", - "account_anchor = FeatureAnchor(name=\"account_features\",\n", - " source=account_info,\n", - " features=features)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "6f12c07e-4faf-4411-8acd-6f5d13b962f8", - "showTitle": false, - "title": "" - } - }, - "source": [ - "### Transaction Features" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "280062b9-ae21-4a1a-ae94-86a5c17fd589", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "# # #Refer to to learn more about the details of each method\n", - "\n", - "transaction_data = HdfsSource(name=\"transaction_data\",\n", - " path=\"wasbs://frauddata@feathrdatastorage.blob.core.windows.net/transaction_out_small.csv\",\n", - " event_timestamp_column=\"transactionDate\",\n", - " timestamp_format=\"yyyyMMdd\")\n", - "\n", - "transaction_id = Feature(name=\"transaction_id\",\n", - " key=accountId,\n", - " feature_type=STRING,\n", - " transform=\"transactionID\")\n", - "\n", - "transaction_currency_code = Feature(name=\"transaction_currency_code\",\n", - " key=accountId,\n", - " feature_type=STRING,\n", - " transform=\"transactionCurrencyCode\")\n", - " \n", - "transaction_amount = Feature(name=\"transaction_amount\",\n", - " key=accountId,\n", - " feature_type=FLOAT,\n", - " transform=\"transactionAmount\")\n", - "\n", - "transaction_device_id = Feature(name=\"transaction_device_id\",\n", - " key=accountId,\n", - " feature_type=FLOAT,\n", - " transform=\"transactionDeviceId\")\n", - "\n", - "transaction_ip_address = Feature(name=\"transaction_ip_address\",\n", - " key=accountId,\n", - " feature_type=FLOAT,\n", - " transform=\"transactionIPaddress\")\n", - "\n", - "transaction_time = Feature(name=\"transaction_time\",\n", - " key=accountId,\n", - " feature_type=INT32,\n", - " transform=\"transactionTime\")\n", - "\n", - "fraud_status = Feature(name=\"fraud_status\",\n", - " key=accountId,\n", - " feature_type=STRING,\n", - " transform=\"fraud_tag\")\n", - "\n", - "features = [\n", - " transaction_id,\n", - " transaction_amount,\n", - " transaction_device_id,\n", - " transaction_ip_address,\n", - " transaction_time,\n", - " transaction_currency_code,\n", - " fraud_status\n", - "]\n", - "\n", - "transaction_feature_anchor = FeatureAnchor(name=\"transaction_features\",\n", - " source=transaction_data,\n", - " features=features)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "86ac05e1-26bb-4820-87ea-f547e3561181", - "showTitle": false, - "title": "" - } - }, - "source": [ - "### Transaction Aggregated Features" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "4c969554-f690-42f5-b70a-d962bf558b03", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "# average amount of transaction in the past week\n", - "transactions_aggr = HdfsSource(name=\"transactions_aggr\",\n", - " path=\"wasbs://frauddata@feathrdatastorage.blob.core.windows.net/transaction_out_small.csv\",\n", - " event_timestamp_column=\"transactionDate\",\n", - " timestamp_format=\"yyyyMMdd\")\n", - "\n", - "# average amount of transaction in that week\n", - "avg_transaction_amount = Feature(name=\"avg_transaction_amount\",\n", - " key=accountId,\n", - " feature_type=FLOAT,\n", - " transform=WindowAggTransformation(agg_expr=\"cast_float(transactionAmount)\",\n", - " agg_func=\"AVG\",\n", - " window=\"7d\"))\n", - "\n", - "# number of transaction that took place in a day\n", - "num_trasaction_count_in_day = Feature(name=\"num_trasaction_count_in_day\",\n", - " key=accountId,\n", - " feature_type=INT32,\n", - " transform=WindowAggTransformation(agg_expr=\"transactionID\",\n", - " agg_func=\"COUNT\",\n", - " window=\"1d\"))\n", - "\n", - "# Amount of transaction that took place in a day\n", - "total_transaction_amount_in_day = Feature(name=\"total_transaction_amount_in_day\",\n", - " key=accountId,\n", - " feature_type=FLOAT,\n", - " transform=WindowAggTransformation(agg_expr=\"cast_float(transactionAmount)\",\n", - " agg_func=\"SUM\",\n", - " window=\"1d\"))\n", - "\n", - "# average time of transaction in the past week\n", - "avg_transaction_time = Feature(name=\"avg_transaction_time\",\n", - " key=accountId,\n", - " feature_type=INT32,\n", - " transform=WindowAggTransformation(agg_expr=\"cast_float(transactionTime)\",\n", - " agg_func=\"AVG\",\n", - " window=\"7d\")) \n", - "\n", - "# total number of currency used for transaction in the past week\n", - "num_currency_type_in_week = Feature(name=\"num_currency_type_in_week\",\n", - " key=accountId,\n", - " feature_type=INT32,\n", - " transform=WindowAggTransformation(agg_expr=\"transactionCurrencyCode\",\n", - " agg_func=\"COUNT\",\n", - " window=\"7d\"))\n", - "\n", - "# number of different ip address used for transaction in the past week\n", - "num_ip_address_count = Feature(name=\"num_ip_address_count\",\n", - " key=accountId,\n", - " feature_type=INT32,\n", - " transform=WindowAggTransformation(agg_expr=\"transactionIPaddress\",\n", - " agg_func=\"COUNT\",\n", - " window=\"7d\"))\n", - "\n", - "# number of devices used for the transaction in the past week\n", - "num_device_count = Feature(name=\"num_device_count\",\n", - " key=accountId,\n", - " feature_type=INT32,\n", - " transform=WindowAggTransformation(agg_expr=\"transactionDeviceId\",\n", - " agg_func=\"COUNT\",\n", - " window=\"7d\"))\n", - "\n", - "# find the time of most recent transaction\n", - "time_most_recent_transaction = Feature(name=\"time_most_recent_transaction\",\n", - " key=accountId,\n", - " feature_type=INT32,\n", - " transform=WindowAggTransformation(agg_expr=\"transactionTime\",\n", - " agg_func=\"LATEST\",\n", - " window=\"7d\"))\n", - "\n", - "features = [\n", - " avg_transaction_amount,\n", - " avg_transaction_time,\n", - " total_transaction_amount_in_day,\n", - " num_trasaction_count_in_day,\n", - " num_currency_type_in_week,\n", - " num_ip_address_count,\n", - " num_device_count,\n", - " time_most_recent_transaction\n", - "]\n", - "\n", - "aggr_anchor = FeatureAnchor(name=\"transaction_aggr_features\",\n", - " source=transactions_aggr,\n", - " features=features)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "17cc5132-461f-4d3d-b517-1f7e69d23252", - "showTitle": false, - "title": "" - } - }, - "source": [ - "### Derived Features\n", - "- `DerivedFeature`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "7ac10ce4-e222-469c-bb2e-1658b45e3eda", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "# derived features\n", - "feature_diff_current_and_avg_amount = DerivedFeature(name=\"feature_diff_current_and_avg_amount\",\n", - " key=accountId,\n", - " feature_type=FLOAT,\n", - " input_features=[\n", - " transaction_amount, avg_transaction_amount],\n", - " transform=\"transaction_amount - avg_transaction_amount\")\n", - "\n", - "feature_time_pass_after_most_recent_transaction = DerivedFeature(name=\"feature_time_pass_after_most_recent_transaction\",\n", - " key=accountId,\n", - " feature_type=INT32,\n", - " input_features=[\n", - " transaction_time, time_most_recent_transaction],\n", - " transform=\"cast_int(transaction_time) - cast_int(time_most_recent_transaction)\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "a9ec8416-9ac6-4499-b60f-55822265b893", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Build Defined Features\n", - "- `build_features`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "d9d32d4f-2b60-4978-bb87-c7d2160e98eb", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "client.build_features(anchor_list=[account_anchor, transaction_feature_anchor, aggr_anchor], \n", - " derived_feature_list=[feature_time_pass_after_most_recent_transaction, feature_diff_current_and_avg_amount])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "fa9e53b9-e7d4-4b25-b486-dc9e6801369a", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Get Offline Features\n", - "- `FeatureQuery`\n", - "- `ObservationSettings`\n", - "- `get_offline_features`\n", - "- `feathr_spark_launcher.download_result`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "b6340f2f-79dc-442b-a202-b2f2078a62ac", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "if client.spark_runtime == 'databricks':\n", - " output_path = 'dbfs:/feathrfrauddetection_test.avro'\n", - "else:\n", - " output_path = feathr_output_path\n", - "\n", - "feature_query = FeatureQuery(\n", - " feature_list=[\"account_country\",\n", - " \"transaction_time\",\n", - " \"num_currency_type_in_week\",\n", - " \"num_trasaction_count_in_day\",\n", - " \"total_transaction_amount_in_day\",\n", - " \"fraud_status\",\n", - " \"is_user_registered\",\n", - " \"avg_transaction_amount\",\n", - " \"num_ip_address_count\",\n", - " \"num_device_count\",\n", - " \"time_most_recent_transaction\",\n", - " \"feature_diff_current_and_avg_amount\",\n", - " \"feature_time_pass_after_most_recent_transaction\"], key=accountId)\n", - " \n", - "settings = ObservationSettings(\n", - " observation_path=\"wasbs://frauddata@feathrdatastorage.blob.core.windows.net/observation_out_small.csv\",\n", - " event_timestamp_column=\"transactionDate\",\n", - " timestamp_format=\"yyyyMMdd\")\n", - " \n", - "client.get_offline_features(observation_settings=settings,\n", - " feature_query=feature_query,\n", - " output_path=output_path)\n", - "client.wait_job_to_finish(timeout_sec=10000000000)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "5b7603ee-0c81-49ed-8e1f-53161ae57cbf", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import pandavro as pdx\n", - "import glob\n", - "from pathlib import Path\n", - "import matplotlib.pyplot as plt\n", - "from datetime import datetime, timedelta\n", - "\n", - "from feathr import BackfillTime, MaterializationSettings, RedisSink" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "997db6eb-c7d8-4f5e-b6e0-09733ff706b7", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "def get_result_df(client: FeathrClient) -> pd.DataFrame:\n", - " \"\"\"Download the job result dataset from cloud as a Pandas dataframe.\"\"\"\n", - " res_url = client.get_job_result_uri(block=True, timeout_sec=600)\n", - " tmp_dir = tempfile.TemporaryDirectory()\n", - " client.feathr_spark_launcher.download_result(result_path=res_url, local_folder=tmp_dir.name)\n", - " dataframe_list = []\n", - " # assuming the result are in avro format\n", - " for file in glob.glob(os.path.join(tmp_dir.name, '*.avro')):\n", - " dataframe_list.append(pdx.read_avro(file))\n", - " vertical_concat_df = pd.concat(dataframe_list, axis=0)\n", - " tmp_dir.cleanup()\n", - " return vertical_concat_df\n", - "\n", - "df_res = get_result_df(client)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "7fff1ac7-90d1-469b-a54c-397904417796", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Feature Visualization" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "e482625e-2ecd-45cb-9d43-5baacd445006", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "filepath = Path('./result_out.csv')\n", - "df_res.to_csv(filepath, index=False) \n", - "df_res.reset_index()\n", - "df_res" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "b4f86c53-16cf-4836-969b-7c34f0922057", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Train Fraud Detection Model with Calculated Features" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "0d9d06b8-01e7-4772-8734-6ebfe1996b03", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "from sklearn.neighbors import KNeighborsClassifier\n", - "from sklearn.model_selection import train_test_split \n", - "import seaborn as sns\n", - "\n", - "final_df = df_res\n", - "final_df.drop(['accountID'], axis=1, inplace=True, errors='ignore')\n", - "final_df.drop(['transactionDate'], axis=1, inplace=True, errors='ignore')\n", - "final_df.drop(['account_country'], axis=1, inplace=True, errors='ignore')\n", - "final_df = final_df.fillna(0)\n", - "\n", - "x_train, x_test, y_train, y_test = train_test_split(final_df.drop([\"fraud_status\"], axis=1),\n", - " final_df[\"fraud_status\"],\n", - " test_size=0.20,\n", - " random_state=0)\n", - " \n", - "K = []\n", - "training = []\n", - "test = []\n", - "scores = {}\n", - " \n", - "for k in range(2, 21):\n", - " clf = KNeighborsClassifier(n_neighbors = k)\n", - " clf.fit(x_train, y_train)\n", - " \n", - " training_score = clf.score(x_train, y_train)\n", - " test_score = clf.score(x_test, y_test)\n", - " K.append(k)\n", - " \n", - " training.append(training_score)\n", - " test.append(test_score)\n", - " scores[k] = [training_score, test_score]\n", - "\n", - "for keys, values in scores.items():\n", - " print(keys, ':', values)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "83e69f23-aa4e-4893-8907-6d5f0792c23f", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Materialize Features in Redis\n", - "- `BackfillTime`\n", - "- `RedisSink`\n", - "- `materialize_features`\n", - "- `multi_get_online_features`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "faad23c1-d827-4674-b630-83530574c27d", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "backfill_time = BackfillTime(start=datetime(\n", - " 2013, 4, 7), end=datetime(2013, 4, 7), step=timedelta(days=1))\n", - "redisSink = RedisSink(table_name=\"fraudDetectionDemoFeature\")\n", - "settings = MaterializationSettings(\"fraudDetectionDemoFeature\",\n", - " backfill_time=backfill_time,\n", - " sinks=[redisSink],\n", - " feature_names=[\"fraud_status\"])\n", - "\n", - "client.materialize_features(settings, allow_materialize_non_agg_feature =True)\n", - "client.wait_job_to_finish(timeout_sec=5000)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "1f5b191f-b1e8-49e4-b54d-ffc2f8c0a0b8", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "client.multi_get_online_features('fraudDetectionDemoFeature', ['1759222192247110', '914800996051170'], [\n", - " \"fraud_status\"])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "9c3b2403-95d6-44a1-b536-d2088608ff58", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "client.multi_get_online_features('fraudDetectionDemoFeature', ['1759222192247110', '914800996051170', '844428033864668'], [\n", - " \"fraud_status\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "71ba8699-3c42-4f73-be59-95b29f468696", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Register Features with Registry APIs\n", - "- `register_features`\n", - "- `list_registered_features`\n", - "- Above queries are send to a Standard Registry API Service (both `Purview` and `SQL` backend are supported)\n", - "- More friendly interface with detailed lineage can be found in: [Feathr UI](https://feathr-sql-registry.azurewebsites.net/)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "c5028dd9-01ed-4394-a5c7-623e674125f6", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "client.register_features()\n", - "client.list_registered_features(project_name=\"fraud_detection_test\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "cb814ce7-72b9-4622-8518-106d4acf9008", - "showTitle": false, - "title": "" - } - }, - "source": [] - } - ], - "metadata": { - "application/vnd.databricks.v1+notebook": { - "dashboards": [], - "language": "python", - "notebookMetadata": { - "pythonIndentUnit": 4 - }, - "notebookName": "fraud_detection_feathr_test_2", - "notebookOrigID": 1891349682974490, - "widgets": {} - }, - "kernelspec": { - "display_name": "Python 3.10.8 ('feathr')", - "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.10.8" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" - } - } - }, - "nbformat": 4, - "nbformat_minor": 0 + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "7b19a0cd-31da-45b7-91a4-9cd561f3d3d8", + "showTitle": false, + "title": "" + } + }, + "source": [ + "# Feathr Fraud Detection Sample\n", + "\n", + "This notebook illustrates the use of Feature Store to create a model that predicts the fraud status of transactions based on the user account data and trasaction data. All the data that was used in the notebook can be found here: https://github.com/microsoft/r-server-fraud-detection.\n", + "\n", + "\n", + "In the following Notebook, we \n", + "1. Install the latest Feathr code (to include some unreleased features) \n", + "2. Define Environment Variables & `yaml_config` Settings \n", + "3. Create `FeathrClient` and Define `FeatureAnchor`\n", + "4. `build_features` and `get_offline_features`\n", + "5. Visualize features and train Fraud Detection Model\n", + "6. `materialize_features` and `get_online_features`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "0b51153e-40dd-43d5-9d3a-501534156e6d", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Setup Feathr Developer Environment" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + ">Prior to running the notebook, if you have not deployed all the required resources, please refer to the guide here and follow the steps to do so: https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-arm.html" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "b9c63dd5-304e-4797-a230-8fb753710dbc", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# Install feathr from the latest codes in the repo. You may use `pip install feathr[notebook]` as well.\n", + "# !pip install \"git+https://github.com/feathr-ai/feathr.git#subdirectory=feathr_project&egg=feathr[notebook]\" " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "69222adf-1cb0-410b-b98d-e22877f358c0", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "from datetime import datetime, timedelta\n", + "import os\n", + "from pathlib import Path\n", + "\n", + "from azure.identity import DefaultAzureCredential\n", + "from azure.keyvault.secrets import SecretClient\n", + "import pandas as pd\n", + "\n", + "import feathr\n", + "from feathr import (\n", + " FeathrClient,\n", + " STRING, BOOLEAN, FLOAT, INT32, ValueType,\n", + " Feature, DerivedFeature, FeatureAnchor,\n", + " BackfillTime, MaterializationSettings,\n", + " FeatureQuery, ObservationSettings,\n", + " RedisSink,\n", + " HdfsSource,\n", + " WindowAggTransformation,\n", + " TypedKey,\n", + ")\n", + "from feathr.datasets.constants import (\n", + " FRAUD_DETECTION_ACCOUNT_INFO_URL,\n", + " FRAUD_DETECTION_FRAUD_TRANSACTIONS_URL,\n", + " FRAUD_DETECTION_UNTAGGED_TRANSACTIONS_URL,\n", + ")\n", + "from feathr.datasets.utils import maybe_download\n", + "from feathr.utils.config import generate_config\n", + "from feathr.utils.job_utils import get_result_df\n", + "\n", + "\n", + "print(f\"Feathr version: {feathr.__version__}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "RESOURCE_PREFIX = \"\" # TODO fill the value used to deploy the resources via ARM template\n", + "PROJECT_NAME = \"fraud_detection\"\n", + "\n", + "# Currently support: 'azure_synapse', 'databricks', and 'local' \n", + "SPARK_CLUSTER = \"local\"\n", + "\n", + "# TODO fill values to use databricks cluster:\n", + "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", + "DATABRICKS_URL = None # Set Databricks workspace url to use databricks\n", + "DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", + "\n", + "# TODO fill values to use Azure Synapse cluster:\n", + "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", + "AZURE_SYNAPSE_URL = None # Set Azure Synapse workspace url to use Azure Synapse\n", + "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", + "\n", + "# An existing Feathr config file path. If None, we'll generate a new config based on the constants in this cell.\n", + "FEATHR_CONFIG_PATH = None\n", + "\n", + "# (For the notebook test pipeline) If true, use ScrapBook package to collect the results.\n", + "SCRAP_RESULTS = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if SPARK_CLUSTER == \"azure_synapse\" and not os.environ.get(\"ADLS_KEY\"):\n", + " os.environ[\"ADLS_KEY\"] = ADLS_KEY\n", + "elif SPARK_CLUSTER == \"databricks\" and not os.environ.get(\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"):\n", + " os.environ[\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"] = DATABRICKS_WORKSPACE_TOKEN_VALUE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "c0299d67-1103-4aa4-ba57-300498ae2579", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "if USE_CLI_AUTH:\n", + " !az login --use-device-code" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Permission\n", + "To run the cells below, you need additional permission: permission to your managed identity to access the keyvault, and permission to the user to access the Storage Blob. Run the following lines of command in the Cloud Shell in order to grant yourself the access.\n", + "\n", + "```\n", + "userId=\n", + "resource_prefix=\n", + "synapse_workspace_name=\"${resource_prefix}syws\"\n", + "keyvault_name=\"${resource_prefix}kv\"\n", + "objectId=$(az ad user show --id $userId --query id -o tsv)\n", + "az keyvault update --name $keyvault_name --enable-rbac-authorization false\n", + "az keyvault set-policy -n $keyvault_name --secret-permissions get list --object-id $objectId\n", + "az role assignment create --assignee $userId --role \"Storage Blob Data Contributor\"\n", + "az synapse role assignment create --workspace-name $synapse_workspace_name --role \"Synapse Contributor\" --assignee $userId\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "a8a70f27-d520-4d3c-bb8c-f364f84cb738", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# Redis password\n", + "if 'REDIS_PASSWORD' not in os.environ:\n", + " # Try to get all the required credentials from Azure Key Vault\n", + " from azure.identity import AzureCliCredential, DefaultAzureCredential \n", + " from azure.keyvault.secrets import SecretClient\n", + "\n", + " # TODO assume the resources are deployed by using the ARM template. If not, please set your vault url name.\n", + " vault_url = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", + " if USE_CLI_AUTH:\n", + " credential = AzureCliCredential(additionally_allowed_tenants=['*'],)\n", + " else:\n", + " credential = DefaultAzureCredential(\n", + " exclude_interactive_browser_credential=False,\n", + " additionally_allowed_tenants=['*'],\n", + " )\n", + " secret_client = SecretClient(vault_url=vault_url, credential=credential)\n", + " retrieved_secret = secret_client.get_secret('FEATHR-ONLINE-STORE-CONN').value\n", + " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "50b2f73e-6380-42c3-91e8-4f3e15bc10d6", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "if FEATHR_CONFIG_PATH:\n", + " config_path = FEATHR_CONFIG_PATH\n", + "else:\n", + " config_path = generate_config(\n", + " resource_prefix=RESOURCE_PREFIX,\n", + " project_name=PROJECT_NAME,\n", + " spark_config__spark_cluster=SPARK_CLUSTER,\n", + " spark_config__azure_synapse__dev_url=AZURE_SYNAPSE_URL,\n", + " spark_config__azure_synapse__pool_name=AZURE_SYNAPSE_SPARK_POOL,\n", + " spark_config__databricks__workspace_instance_url=DATABRICKS_URL,\n", + " databricks_cluster_id=DATABRICKS_CLUSTER_ID,\n", + " )\n", + "\n", + "with open(config_path, 'r') as f: \n", + " print(f.read())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "eab0957c-c906-4297-a729-8dd8d79cb629", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Initialize Feathr Client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "3734eee3-12f9-44db-a440-ad375ef859f0", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "client = FeathrClient(config_path=config_path, credential=credential)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prepare Datasets\n", + "\n", + "1. Download Account info data, fraud transactions data, and untagged transactions data.\n", + "2. Merge two transactions data (fraud and untagged) into one\n", + "3. Upload data files to cloud so that the target cluster can consume" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Download datasets TODO tmp folder for databricks?\n", + "account_info_file_path = str(Path(PROJECT_NAME, \"account_info.csv\"))\n", + "fraud_transactions_file_path = str(Path(PROJECT_NAME, \"fraud_transactions.csv\"))\n", + "obs_transactions_file_path = str(Path(PROJECT_NAME, \"obs_transactions.csv\"))\n", + "maybe_download(\n", + " src_url=FRAUD_DETECTION_ACCOUNT_INFO_URL,\n", + " dst_filepath=account_info_file_path,\n", + ")\n", + "maybe_download(\n", + " src_url=FRAUD_DETECTION_FRAUD_TRANSACTIONS_URL,\n", + " dst_filepath=fraud_transactions_file_path,\n", + ")\n", + "maybe_download(\n", + " src_url=FRAUD_DETECTION_UNTAGGED_TRANSACTIONS_URL,\n", + " dst_filepath=obs_transactions_file_path,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Concat fraud and obs transactions\n", + "fraud_df = pd.read_csv(fraud_transactions_file_path)\n", + "fraud_df[\"fraud_tag\"] = \"Fraud\"\n", + "obs_df = pd.read_csv(obs_transactions_file_path)\n", + "obs_df[\"fraud_tag\"] = \"Unknown\"\n", + "\n", + "transactions_file_path = str(Path(PROJECT_NAME, \"transactions.csv\"))\n", + "transactions_df = pd.concat([fraud_df, obs_df], ignore_index=True)\n", + "transactions_df.to_csv(transactions_file_path, index=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Upload files to cloud if needed\n", + "if client.spark_runtime == \"local\" or (client.spark_runtime == \"databricks\" and is_databricks()):\n", + " # In local mode, we can use the same data path as the source.\n", + " # If the notebook is running on databricks, DATA_FILE_PATH should be already a dbfs path.\n", + " account_info_source_path = account_info_file_path\n", + " transactions_source_path = transactions_file_path\n", + "else:\n", + " # Otherwise, upload the local file to the cloud storage (either dbfs or adls).\n", + " account_info_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(account_info_file_path)\n", + " transactions_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(transactions_file_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "f6adbca1-5642-4ac1-bff7-e7c9d4d9e5b2", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Define Features\n", + "\n", + "Now, we define following features:\n", + "- Account features: Account-level features that will be joined to observation data on accountID\n", + "- Transaction features: The features that will be joined to observation data on transactionID\n", + "- Transaction aggregated features: The features aggregated by accountID\n", + "- Derived features: The features derived from other features\n", + "\n", + "Some important concepts include `HdfsSource`, `TypedKey`, `Feature`, `FeatureAnchor`, and `DerivedFeature`. Please refer to feathr [documents](https://feathr.readthedocs.io/en/latest/feathr.html) to learn more about the details.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "b073b509-0f95-4e23-b16b-ffd8190fb6a2", + "showTitle": false, + "title": "" + } + }, + "source": [ + "### Account Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check account data\n", + "pd.read_csv(account_info_file_path).head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def account_dropna(df):\n", + " \"\"\"Drop rows with missing values in the account info dataset.\"\"\"\n", + " return df.select(\n", + " \"accountID\",\n", + " \"transactionDate\",\n", + " \"accountCountry\",\n", + " \"isUserRegistered\",\n", + " \"numPaymentRejects1dPerUser\",\n", + " \"accountAge\",\n", + " ).dropna()\n", + "\n", + "\n", + "account_info_source = HdfsSource(\n", + " name=\"account_data\",\n", + " path=account_info_source_path,\n", + " event_timestamp_column=\"transactionDate\",\n", + " timestamp_format=\"yyyyMMdd\",\n", + " preprocessing=account_dropna,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "b3668eeb-e4a0-4327-baf6-5521c856f51d", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# Account features will be joined to observation data on accountID\n", + "account_id = TypedKey(\n", + " key_column=\"accountID\",\n", + " key_column_type=ValueType.STRING,\n", + " description=\"account id\",\n", + ")\n", + " \n", + "account_features = [\n", + " Feature(\n", + " name=\"account_country\",\n", + " key=account_id,\n", + " feature_type=STRING, \n", + " transform=\"accountCountry\",\n", + " ),\n", + " Feature(\n", + " name=\"is_user_registered\",\n", + " key=account_id,\n", + " feature_type=BOOLEAN,\n", + " transform=\"isUserRegistered==TRUE\",\n", + " ),\n", + " Feature(\n", + " name=\"num_payment_rejects_1d_per_user\",\n", + " key=account_id,\n", + " feature_type=INT32,\n", + " transform=\"numPaymentRejects1dPerUser\",\n", + " ),\n", + " Feature(\n", + " name=\"account_age\",\n", + " key=account_id,\n", + " feature_type=INT32,\n", + " transform=\"accountAge\",\n", + " ),\n", + "]\n", + "\n", + "account_anchor = FeatureAnchor(\n", + " name=\"account_features\",\n", + " source=account_info_source,\n", + " features=account_features,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "6f12c07e-4faf-4411-8acd-6f5d13b962f8", + "showTitle": false, + "title": "" + } + }, + "source": [ + "### Transaction Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Check transaction data\n", + "pd.read_csv(transactions_file_path).head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def transaction_dropna(df):\n", + " \"\"\"Drop rows with missing values in the transactions dataset.\"\"\"\n", + " return df.dropna(subset=[\n", + " \"accountID\",\n", + " \"transactionDate\",\n", + " \"transactionID\",\n", + " \"transactionCurrencyCode\",\n", + " \"transactionAmount\",\n", + " \"transactionTime\",\n", + " \"ipCountryCode\",\n", + " ])\n", + "\n", + "\n", + "transactions_source = HdfsSource(\n", + " name=\"transaction_data\",\n", + " path=transactions_source_path,\n", + " event_timestamp_column=\"transactionDate\",\n", + " timestamp_format=\"yyyyMMdd\",\n", + " preprocessing=transaction_dropna,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "280062b9-ae21-4a1a-ae94-86a5c17fd589", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# Transaction features will be joined to observation data on transactionID\n", + "transaction_id = TypedKey(\n", + " key_column=\"transactionID\",\n", + " key_column_type=ValueType.STRING,\n", + " description=\"transaction id\",\n", + ")\n", + "\n", + "transaction_amount = Feature(\n", + " name=\"transaction_amount\",\n", + " key=transaction_id,\n", + " feature_type=FLOAT,\n", + " transform=\"transactionAmount\",\n", + ")\n", + "\n", + "transaction_features = [\n", + " transaction_amount,\n", + " Feature(\n", + " name=\"transaction_ip_country_code\",\n", + " key=transaction_id,\n", + " feature_type=STRING,\n", + " transform=\"ipCountryCode\",\n", + " ),\n", + " Feature(\n", + " name=\"transaction_currency_code\",\n", + " key=transaction_id,\n", + " feature_type=STRING,\n", + " transform=\"transactionCurrencyCode\",\n", + " ),\n", + " Feature(\n", + " name=\"transaction_time\",\n", + " key=transaction_id,\n", + " feature_type=INT32,\n", + " transform=\"transactionTime\",\n", + " ),\n", + "]\n", + "\n", + "transaction_feature_anchor = FeatureAnchor(\n", + " name=\"transaction_features\",\n", + " source=transactions_source,\n", + " features=transaction_features,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "86ac05e1-26bb-4820-87ea-f547e3561181", + "showTitle": false, + "title": "" + } + }, + "source": [ + "### Transaction Aggregated Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "4c969554-f690-42f5-b70a-d962bf558b03", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# average amount of transaction in that week\n", + "avg_transaction_amount = Feature(\n", + " name=\"avg_transaction_amount\",\n", + " key=account_id,\n", + " feature_type=FLOAT,\n", + " transform=WindowAggTransformation(\n", + " agg_expr=\"cast_float(transactionAmount)\", agg_func=\"AVG\", window=\"7d\"\n", + " ),\n", + ")\n", + "\n", + "agg_features = [\n", + " avg_transaction_amount,\n", + " # number of transaction that took place in a day\n", + " Feature(\n", + " name=\"num_transaction_count_in_day\",\n", + " key=account_id,\n", + " feature_type=INT32,\n", + " transform=WindowAggTransformation(\n", + " agg_expr=\"transactionID\", agg_func=\"COUNT\", window=\"1d\"\n", + " ),\n", + " ),\n", + " # number of transaction that took place in the past week\n", + " Feature(\n", + " name=\"num_transaction_count_in_week\",\n", + " key=account_id,\n", + " feature_type=INT32,\n", + " transform=WindowAggTransformation(\n", + " agg_expr=\"transactionID\", agg_func=\"COUNT\", window=\"7d\"\n", + " ),\n", + " ),\n", + " # amount of transaction that took place in a day\n", + " Feature(\n", + " name=\"total_transaction_amount_in_day\",\n", + " key=account_id,\n", + " feature_type=FLOAT,\n", + " transform=WindowAggTransformation(\n", + " agg_expr=\"cast_float(transactionAmount)\", agg_func=\"SUM\", window=\"1d\"\n", + " ),\n", + " ),\n", + " # average time of transaction in the past week\n", + " Feature(\n", + " name=\"avg_transaction_time\",\n", + " key=account_id,\n", + " feature_type=FLOAT,\n", + " transform=WindowAggTransformation(\n", + " agg_expr=\"cast_float(transactionTime)\", agg_func=\"AVG\", window=\"7d\"\n", + " ),\n", + " ),\n", + "]\n", + "\n", + "agg_anchor = FeatureAnchor(\n", + " name=\"transaction_agg_features\",\n", + " source=transactions_source,\n", + " features=agg_features,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "17cc5132-461f-4d3d-b517-1f7e69d23252", + "showTitle": false, + "title": "" + } + }, + "source": [ + "### Derived Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "7ac10ce4-e222-469c-bb2e-1658b45e3eda", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "derived_features = [\n", + " DerivedFeature(\n", + " name=\"feature_diff_current_and_avg_amount\",\n", + " key=[transaction_id, account_id],\n", + " feature_type=FLOAT,\n", + " input_features=[transaction_amount, avg_transaction_amount],\n", + " transform=\"transaction_amount - avg_transaction_amount\",\n", + " )\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "a9ec8416-9ac6-4499-b60f-55822265b893", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Build and Get Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "d9d32d4f-2b60-4978-bb87-c7d2160e98eb", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "client.build_features(\n", + " anchor_list=[\n", + " account_anchor,\n", + " transaction_feature_anchor,\n", + " agg_anchor,\n", + " ],\n", + " derived_feature_list=derived_features,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "account_feature_names = [feat.name for feat in account_features] + [feat.name for feat in agg_features]\n", + "transactions_feature_names = [feat.name for feat in transaction_features]\n", + "derived_feature_names = [feat.name for feat in derived_features]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "b6340f2f-79dc-442b-a202-b2f2078a62ac", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "account_feature_query = FeatureQuery(\n", + " feature_list=account_feature_names,\n", + " key=account_id,\n", + ")\n", + "\n", + "transactions_feature_query = FeatureQuery(\n", + " feature_list=transactions_feature_names,\n", + " key=transaction_id,\n", + ")\n", + "\n", + "derived_feature_query = FeatureQuery(\n", + " feature_list=derived_feature_names,\n", + " key=[transaction_id, account_id],\n", + ")\n", + " \n", + "settings = ObservationSettings(\n", + " observation_path=transactions_source_path,\n", + " event_timestamp_column=\"transactionDate\",\n", + " timestamp_format=\"yyyyMMdd\",\n", + ")\n", + " \n", + "client.get_offline_features(\n", + " observation_settings=settings,\n", + " feature_query=[account_feature_query, transactions_feature_query, derived_feature_query],\n", + " output_path=transactions_source_path.rpartition(\"/\")[0] + f\"/fraud_transactions_features.avro\",\n", + ")\n", + "\n", + "client.wait_job_to_finish(timeout_sec=5000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = get_result_df(client)[\n", + " account_feature_names\n", + " + transactions_feature_names\n", + " + derived_feature_names\n", + " + [\"accountID\", \"transactionID\", \"fraud_tag\"]\n", + "]\n", + "\n", + "# Data cleaning: Remove the records if the account does not exist in the account info dataset\n", + "df.dropna(subset=[\"is_user_registered\"], inplace=True)\n", + "\n", + "df.head(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df.nunique()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Feature Visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import plotly.express as px\n", + "\n", + "\n", + "NUM_SAMPLES_TO_PLOT = 10000" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = px.scatter_matrix(\n", + " df[:NUM_SAMPLES_TO_PLOT],\n", + " dimensions=account_feature_names + transactions_feature_names + derived_feature_names,\n", + " color=\"fraud_tag\",\n", + " title=\"Scatter matrix of transaction dataset\",\n", + ")\n", + "fig.update_traces(diagonal_visible=False, marker_size=3)\n", + "fig.update_layout(\n", + " width=1000,\n", + " height=1000,\n", + " font_size=5,\n", + ")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Build Fraud Detection Model\n", + "\n", + "In this notebook, we train one-class Support Vector Machine (SVM) to score the transactions, where the score results can be used to determine if the transactions are fraud or not.\n", + "\n", + "### Feature Preprocessing\n", + "\n", + "Before we input the features to the model, we convert categorical features into neumeric vectors (using a simple one-hot encoding technique) and do standard scaling all the features." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from sklearn.preprocessing import OneHotEncoder, StandardScaler" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Split fraud (train) and observation (test) datasets\n", + "fraud_df = df[df[\"fraud_tag\"]==\"Fraud\"].drop([\"accountID\", \"transactionID\", \"fraud_tag\"], axis=\"columns\") \n", + "obs_df = df[df[\"fraud_tag\"]==\"Unknown\"].drop([\"accountID\", \"transactionID\", \"fraud_tag\"], axis=\"columns\") \n", + "print(f\"Num fraud samples = {len(fraud_df)}\", f\"Num untagged samples = {len(obs_df)}\", sep=\"\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Feature names to encode\n", + "enc_feature_names = [\"account_country\", \"is_user_registered\", \"transaction_ip_country_code\", \"transaction_currency_code\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define and fit one-hot encoder\n", + "enc = OneHotEncoder(handle_unknown=\"ignore\").fit(fraud_df[enc_feature_names])\n", + "\n", + "fraud_features = np.concatenate(\n", + " (\n", + " # Encoded features\n", + " enc.transform(fraud_df[enc_feature_names]).toarray(),\n", + " # Other features that don't need to be encoded\n", + " fraud_df.drop(enc_feature_names, axis=\"columns\").fillna(0).to_numpy(),\n", + " \n", + " ),\n", + " axis=1,\n", + ")\n", + "\n", + "# Define and fit standard scaler\n", + "scaler = StandardScaler().fit(fraud_features)\n", + "\n", + "fraud_features = scaler.transform(fraud_features)\n", + "print(f\"A sample of fraud feature:\\n{fraud_features[0]}\\nData shape = {fraud_features.shape}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "obs_features = np.concatenate(\n", + " (\n", + " # Encoded features\n", + " enc.transform(obs_df[enc_feature_names]).toarray(),\n", + " # Other features that don't need to be encoded\n", + " obs_df.drop(enc_feature_names, axis=\"columns\").fillna(0).to_numpy(),\n", + " \n", + " ),\n", + " axis=1,\n", + ")\n", + "obs_features = scaler.transform(obs_features)\n", + "print(f\"A sample of observation feature:\\n{obs_features[0]}\\nData shape = {obs_features.shape}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "7fff1ac7-90d1-469b-a54c-397904417796", + "showTitle": false, + "title": "" + } + }, + "source": [ + "### Model Training" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.svm import OneClassSVM\n", + "\n", + "clf = OneClassSVM(nu=0.1, kernel=\"rbf\", gamma=0.1).fit(fraud_features)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data Scoring" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fraud_feature_scores = clf.score_samples(fraud_features)\n", + "fraud_feature_scores" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "obs_feature_scores = clf.score_samples(obs_features)\n", + "obs_feature_scores" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Result Visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.manifold import TSNE" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# We subsample observation transaction data for visualization\n", + "obs_sample_idx = np.random.choice(len(obs_features), size=NUM_SAMPLES_TO_PLOT, replace=False)\n", + "obs_sample_idx" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scores = np.concatenate([fraud_feature_scores, obs_feature_scores[obs_sample_idx]], axis=0)\n", + "scores.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "features = np.concatenate([fraud_features, obs_features[obs_sample_idx]], axis=0)\n", + "features.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tsne = TSNE(n_components=2, perplexity=50, n_iter=300)\n", + "tsne_results = tsne.fit_transform(features)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = px.scatter(x=tsne_results[:, 0], y=tsne_results[:, 1], color=scores)\n", + "fig.update_traces(marker_size=5, marker_opacity=0.5)\n", + "fig.update_layout(\n", + " width=800,\n", + " height=800,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "83e69f23-aa4e-4893-8907-6d5f0792c23f", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Materialize Features in Redis\n", + "\n", + "Now, we materialize features to `RedisSink` so that we can retrieve online features." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "faad23c1-d827-4674-b630-83530574c27d", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "ACCOUNT_FEATURE_TABLE_NAME = \"fraudDetectionAccountFeatures\" \n", + "\n", + "backfill_time = BackfillTime(\n", + " start=datetime(2013, 7, 31),\n", + " end=datetime(2013, 7, 31),\n", + " step=timedelta(days=1),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "client.materialize_features(\n", + " MaterializationSettings(\n", + " ACCOUNT_FEATURE_TABLE_NAME,\n", + " backfill_time=backfill_time,\n", + " sinks=[RedisSink(table_name=ACCOUNT_FEATURE_TABLE_NAME)],\n", + " feature_names=account_feature_names,\n", + " ),\n", + " allow_materialize_non_agg_feature=True,\n", + ")\n", + "\n", + "client.wait_job_to_finish(timeout_sec=5000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "inputWidgets": {}, + "nuid": "1f5b191f-b1e8-49e4-b54d-ffc2f8c0a0b8", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "materialized_feature_values = client.get_online_features(\n", + " ACCOUNT_FEATURE_TABLE_NAME,\n", + " key=\"A1055520429343950\",\n", + " feature_names=account_feature_names,\n", + ")\n", + "materialized_feature_values" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Scrap results for unit test" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if SCRAP_RESULTS:\n", + " import scrapbook as sb\n", + " sb.glue(\"materialized_feature_values\", materialized_feature_values)" + ] + } + ], + "metadata": { + "application/vnd.databricks.v1+notebook": { + "dashboards": [], + "language": "python", + "notebookMetadata": { + "pythonIndentUnit": 4 + }, + "notebookName": "fraud_detection_feathr_test_2", + "notebookOrigID": 1891349682974490, + "widgets": {} + }, + "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.10.8" + }, + "vscode": { + "interpreter": { + "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 } diff --git a/docs/samples/nyc_taxi_demo.ipynb b/docs/samples/nyc_taxi_demo.ipynb index fc4593e77..2555deaf6 100644 --- a/docs/samples/nyc_taxi_demo.ipynb +++ b/docs/samples/nyc_taxi_demo.ipynb @@ -525,7 +525,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Upload files to cloud\n", + "# Upload files to cloud if needed\n", "if client.spark_runtime == \"local\" or (client.spark_runtime == \"databricks\" and is_databricks()):\n", " # In local mode, we can use the same data path as the source.\n", " # If the notebook is running on databricks, DATA_FILE_PATH should be already a dbfs path.\n", @@ -550,8 +550,8 @@ " name=\"nycTaxiBatchSource\",\n", " path=data_source_path,\n", " event_timestamp_column=TIMESTAMP_COL,\n", - " preprocessing=preprocessing,\n", " timestamp_format=TIMESTAMP_FORMAT,\n", + " preprocessing=preprocessing,\n", ")" ] }, @@ -728,8 +728,7 @@ "metadata": {}, "outputs": [], "source": [ - "DATA_FORMAT = \"parquet\"\n", - "offline_features_path = str(Path(DATA_STORE_PATH, \"feathr_output\", f\"features.{DATA_FORMAT}\"))" + "DATA_FORMAT = \"parquet\"" ] }, { @@ -763,7 +762,7 @@ " execution_configurations=SparkExecutionConfiguration({\n", " \"spark.feathr.outputFormat\": DATA_FORMAT,\n", " }),\n", - " output_path=offline_features_path,\n", + " output_path=data_source_path.rpartition(\"/\")[0] + f\"/features.{DATA_FORMAT}\",\n", ")\n", "\n", "client.wait_job_to_finish(timeout_sec=5000)" @@ -780,7 +779,6 @@ " spark=spark,\n", " client=client,\n", " data_format=DATA_FORMAT,\n", - " res_url=offline_features_path,\n", ")\n", "df.select(feature_names).limit(5).toPandas()" ] @@ -1122,7 +1120,7 @@ }, "celltoolbar": "Tags", "kernelspec": { - "display_name": "Python 3.10.8 ('feathr')", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1140,7 +1138,7 @@ }, "vscode": { "interpreter": { - "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" + "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" } } }, diff --git a/feathr_project/feathr/client.py b/feathr_project/feathr/client.py index ac76f6b5a..8e8301fd5 100644 --- a/feathr_project/feathr/client.py +++ b/feathr_project/feathr/client.py @@ -543,7 +543,7 @@ def _get_offline_features_with_config(self, - Job configuration are like "configurations" for the spark job and are usually spark specific. For example, we want to control the no. of write parts for spark Job configurations and job arguments (or sometimes called job parameters) have quite some overlaps (i.e. you can achieve the same goal by either using the job arguments/parameters vs. job configurations). But the job tags should just be used for metadata purpose. ''' - + # submit the jars return self.feathr_spark_launcher.submit_feathr_job( job_name=self.project_name + '_feathr_feature_join_job', @@ -673,7 +673,7 @@ def materialize_features(self, settings: MaterializationSettings, execution_conf if len(feature_list) > 0: if 'anchor_list' in dir(self): anchors = [anchor for anchor in self.anchor_list if isinstance(anchor.source, InputContext)] - anchor_feature_names = set(feature.name for anchor in anchors for feature in anchor.features) + anchor_feature_names = set(feature.name for anchor in anchors for feature in anchor.features) for feature in feature_list: if feature in anchor_feature_names: raise RuntimeError(f"Materializing features that are defined on INPUT_CONTEXT is not supported. {feature} is defined on INPUT_CONTEXT so you should remove it from the feature list in MaterializationSettings.") diff --git a/feathr_project/feathr/datasets/constants.py b/feathr_project/feathr/datasets/constants.py index 873afe341..13c33713c 100644 --- a/feathr_project/feathr/datasets/constants.py +++ b/feathr_project/feathr/datasets/constants.py @@ -6,6 +6,10 @@ "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/frauddetection/Account_Info.csv" ) -FRAUD_DETECTION_TRANSACTIONS_URL = ( +FRAUD_DETECTION_FRAUD_TRANSACTIONS_URL = ( "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/frauddetection/Fraud_Transactions.csv" ) + +FRAUD_DETECTION_UNTAGGED_TRANSACTIONS_URL = ( + "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/frauddetection/Untagged_Transactions.csv" +) diff --git a/feathr_project/test/samples/test_notebooks.py b/feathr_project/test/samples/test_notebooks.py index c47076fde..0551abd35 100644 --- a/feathr_project/test/samples/test_notebooks.py +++ b/feathr_project/test/samples/test_notebooks.py @@ -21,6 +21,7 @@ NOTEBOOK_PATHS = { "nyc_taxi_demo": str(SAMPLES_DIR.joinpath("nyc_taxi_demo.ipynb")), "feature_embedding": str(SAMPLES_DIR.joinpath("feature_embedding.ipynb")), + "fraud_detection_demo": str(SAMPLES_DIR.joinpath("fraud_detection_demo.ipynb")), } @@ -76,3 +77,31 @@ def test__feature_embedding(config_path, tmp_path): CLEAN_UP=True, ), ) + + +@pytest.mark.notebooks +def test__fraud_detection_demo(config_path, tmp_path): + notebook_name = "fraud_detection_demo" + + output_tmpdir = TemporaryDirectory() + output_notebook_path = str(tmp_path.joinpath(f"{notebook_name}.ipynb")) + + print(f"Running {notebook_name} notebook as {output_notebook_path}") + + pm.execute_notebook( + input_path=NOTEBOOK_PATHS[notebook_name], + output_path=output_notebook_path, + # kernel_name="python3", + parameters=dict( + FEATHR_CONFIG_PATH=config_path, + DATA_STORE_PATH=output_tmpdir.name, + USE_CLI_AUTH=False, + SCRAP_RESULTS=True, + ), + ) + + # Read results from the Scrapbook and assert expected values + nb = sb.read_notebook(output_notebook_path) + outputs = nb.scraps + + assert outputs["materialized_feature_values"].data == pytest.approx(['GB', False, 0, 2000, 0.0, 0, 0, 0.0, 0.0], abs=1.) From 5e6a6bc9d4b4625baac78102140c6af1ea750200 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Tue, 13 Dec 2022 06:56:18 +0000 Subject: [PATCH 04/22] WIP debugging Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/feature_embedding.ipynb | 2 +- docs/samples/fraud_detection_demo.ipynb | 63 +++++++++---------- docs/samples/nyc_taxi_demo.ipynb | 37 ++++++----- feathr_project/test/samples/test_notebooks.py | 4 +- 4 files changed, 54 insertions(+), 52 deletions(-) diff --git a/docs/samples/feature_embedding.ipynb b/docs/samples/feature_embedding.ipynb index 34ffa2a60..3936d0015 100755 --- a/docs/samples/feature_embedding.ipynb +++ b/docs/samples/feature_embedding.ipynb @@ -791,7 +791,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.10.8 (main, Nov 24 2022, 14:13:03) [GCC 11.2.0]" }, "vscode": { "interpreter": { diff --git a/docs/samples/fraud_detection_demo.ipynb b/docs/samples/fraud_detection_demo.ipynb index ff344577d..8e9fd0d4c 100644 --- a/docs/samples/fraud_detection_demo.ipynb +++ b/docs/samples/fraud_detection_demo.ipynb @@ -80,8 +80,6 @@ "import os\n", "from pathlib import Path\n", "\n", - "from azure.identity import DefaultAzureCredential\n", - "from azure.keyvault.secrets import SecretClient\n", "import pandas as pd\n", "\n", "import feathr\n", @@ -135,6 +133,8 @@ "AZURE_SYNAPSE_URL = None # Set Azure Synapse workspace url to use Azure Synapse\n", "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", "\n", + "USE_CLI_AUTH = False # Set to True to use CLI authentication\n", + "\n", "# An existing Feathr config file path. If None, we'll generate a new config based on the constants in this cell.\n", "FEATHR_CONFIG_PATH = None\n", "\n", @@ -154,23 +154,6 @@ " os.environ[\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"] = DATABRICKS_WORKSPACE_TOKEN_VALUE" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "c0299d67-1103-4aa4-ba57-300498ae2579", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "if USE_CLI_AUTH:\n", - " !az login --use-device-code" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -203,22 +186,36 @@ } }, "outputs": [], + "source": [ + "# Get an authentication credential to access Azure resources and register features\n", + "if USE_CLI_AUTH:\n", + " # Use AZ CLI interactive browser authentication\n", + " !az login --use-device-code\n", + " from azure.identity import AzureCliCredential\n", + " credential = AzureCliCredential(additionally_allowed_tenants=['*'],)\n", + "elif \"AZURE_TENANT_ID\" in os.environ and \"AZURE_CLIENT_ID\" in os.environ and \"AZURE_CLIENT_SECRET\" in os.environ:\n", + " # Use Environment variable secret\n", + " from azure.identity import EnvironmentCredential\n", + " credential = EnvironmentCredential()\n", + "else:\n", + " # Try to use the default credential\n", + " from azure.identity import DefaultAzureCredential\n", + " credential = DefaultAzureCredential(\n", + " exclude_interactive_browser_credential=False,\n", + " additionally_allowed_tenants=['*'],\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "# Redis password\n", "if 'REDIS_PASSWORD' not in os.environ:\n", - " # Try to get all the required credentials from Azure Key Vault\n", - " from azure.identity import AzureCliCredential, DefaultAzureCredential \n", " from azure.keyvault.secrets import SecretClient\n", - "\n", - " # TODO assume the resources are deployed by using the ARM template. If not, please set your vault url name.\n", " vault_url = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", - " if USE_CLI_AUTH:\n", - " credential = AzureCliCredential(additionally_allowed_tenants=['*'],)\n", - " else:\n", - " credential = DefaultAzureCredential(\n", - " exclude_interactive_browser_credential=False,\n", - " additionally_allowed_tenants=['*'],\n", - " )\n", " secret_client = SecretClient(vault_url=vault_url, credential=credential)\n", " retrieved_secret = secret_client.get_secret('FEATHR-ONLINE-STORE-CONN').value\n", " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]" @@ -1194,7 +1191,7 @@ "widgets": {} }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "feathr", "language": "python", "name": "python3" }, @@ -1208,11 +1205,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.8 (main, Nov 24 2022, 14:13:03) [GCC 11.2.0]" }, "vscode": { "interpreter": { - "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" + "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" } } }, diff --git a/docs/samples/nyc_taxi_demo.ipynb b/docs/samples/nyc_taxi_demo.ipynb index 2555deaf6..90296b68d 100644 --- a/docs/samples/nyc_taxi_demo.ipynb +++ b/docs/samples/nyc_taxi_demo.ipynb @@ -244,8 +244,23 @@ "metadata": {}, "outputs": [], "source": [ + "# Get an authentication credential to access Azure resources and register features\n", "if USE_CLI_AUTH:\n", - " !az login --use-device-code" + " # Use AZ CLI interactive browser authentication\n", + " !az login --use-device-code\n", + " from azure.identity import AzureCliCredential\n", + " credential = AzureCliCredential(additionally_allowed_tenants=['*'],)\n", + "elif \"AZURE_TENANT_ID\" in os.environ and \"AZURE_CLIENT_ID\" in os.environ and \"AZURE_CLIENT_SECRET\" in os.environ:\n", + " # Use Environment variable secret\n", + " from azure.identity import EnvironmentCredential\n", + " credential = EnvironmentCredential()\n", + "else:\n", + " # Try to use the default credential\n", + " from azure.identity import DefaultAzureCredential\n", + " credential = DefaultAzureCredential(\n", + " exclude_interactive_browser_credential=False,\n", + " additionally_allowed_tenants=['*'],\n", + " )" ] }, { @@ -256,21 +271,11 @@ "source": [ "# Redis password\n", "if 'REDIS_PASSWORD' not in os.environ:\n", - " # Try to get all the required credentials from Azure Key Vault\n", - " from azure.identity import AzureCliCredential, DefaultAzureCredential \n", " from azure.keyvault.secrets import SecretClient\n", - "\n", " vault_url = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", - " if USE_CLI_AUTH:\n", - " credential = AzureCliCredential(additionally_allowed_tenants=['*'],)\n", - " else:\n", - " credential = DefaultAzureCredential(\n", - " exclude_interactive_browser_credential=False,\n", - " additionally_allowed_tenants=['*'],\n", - " )\n", " secret_client = SecretClient(vault_url=vault_url, credential=credential)\n", " retrieved_secret = secret_client.get_secret('FEATHR-ONLINE-STORE-CONN').value\n", - " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]\n" + " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]" ] }, { @@ -353,7 +358,7 @@ }, "outputs": [], "source": [ - "client = FeathrClient(config_path=config_path)" + "client = FeathrClient(config_path=config_path, credential=credential)" ] }, { @@ -1120,7 +1125,7 @@ }, "celltoolbar": "Tags", "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "feathr", "language": "python", "name": "python3" }, @@ -1134,11 +1139,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.8 (main, Nov 24 2022, 14:13:03) [GCC 11.2.0]" }, "vscode": { "interpreter": { - "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" + "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" } } }, diff --git a/feathr_project/test/samples/test_notebooks.py b/feathr_project/test/samples/test_notebooks.py index 0551abd35..80947009a 100644 --- a/feathr_project/test/samples/test_notebooks.py +++ b/feathr_project/test/samples/test_notebooks.py @@ -84,7 +84,7 @@ def test__fraud_detection_demo(config_path, tmp_path): notebook_name = "fraud_detection_demo" output_tmpdir = TemporaryDirectory() - output_notebook_path = str(tmp_path.joinpath(f"{notebook_name}.ipynb")) + output_notebook_path = "output.ipynb" # TODO str(tmp_path.joinpath(f"{notebook_name}.ipynb")) print(f"Running {notebook_name} notebook as {output_notebook_path}") @@ -94,7 +94,7 @@ def test__fraud_detection_demo(config_path, tmp_path): # kernel_name="python3", parameters=dict( FEATHR_CONFIG_PATH=config_path, - DATA_STORE_PATH=output_tmpdir.name, + DATA_STORE_PATH=output_tmpdir.name, # TODO we don't have this <-- USE_CLI_AUTH=False, SCRAP_RESULTS=True, ), From 711090fb4c0296b5faf024635b531949260f0725 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Tue, 13 Dec 2022 11:59:18 +0000 Subject: [PATCH 05/22] Update notebooks Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/feature_embedding.ipynb | 45 +++++++-------- docs/samples/fraud_detection_demo.ipynb | 51 ++++++++++++++--- docs/samples/nyc_taxi_demo.ipynb | 56 +++++++++++-------- feathr_project/test/samples/test_notebooks.py | 8 +-- 4 files changed, 101 insertions(+), 59 deletions(-) diff --git a/docs/samples/feature_embedding.ipynb b/docs/samples/feature_embedding.ipynb index 3936d0015..37a0a5041 100755 --- a/docs/samples/feature_embedding.ipynb +++ b/docs/samples/feature_embedding.ipynb @@ -126,10 +126,7 @@ "# TODO fill the values to use EnvironmentCredential for authentication. (e.g. to run this notebook on DataBricks.)\n", "AZURE_TENANT_ID = None\n", "AZURE_CLIENT_ID = None\n", - "AZURE_CLIENT_SECRET = None\n", - "\n", - "# Set True to delete the project output files at the end of this notebook.\n", - "CLEAN_UP = False" + "AZURE_CLIENT_SECRET = None" ] }, { @@ -195,6 +192,19 @@ "DATA_URL = \"https://azurefeathrstorage.blob.core.windows.net/public/sample_data/hotel_reviews_100_with_id.csv\"" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use dbfs if the notebook is running on Databricks\n", + "if is_databricks():\n", + " WORKING_DIR = f\"/dbfs/{PROJECT_NAME}\"\n", + "else:\n", + " WORKING_DIR = PROJECT_NAME" + ] + }, { "cell_type": "code", "execution_count": null, @@ -209,15 +219,7 @@ }, "outputs": [], "source": [ - "if is_databricks():\n", - " data_filepath = f\"/dbfs/{PROJECT_NAME}/hotel_reviews_100_with_id.csv\"\n", - "elif is_jupyter():\n", - " data_filepath = f\"{PROJECT_NAME}/hotel_reviews_100_with_id.csv\"\n", - "else:\n", - " # This notebook is supposed to be run on Databricks or Jupyter.\n", - " # Note, you still can use Azure Synapse for the target Spark cluster.\n", - " raise ValueError(\"Unsupported platform\")\n", - "\n", + "data_filepath = f\"{WORKING_DIR}/hotel_reviews_100_with_id.csv\"\n", "maybe_download(src_url=DATA_URL, dst_filepath=data_filepath)" ] }, @@ -289,9 +291,11 @@ " # You may set an existing cluster id here, but Databricks recommend to use new clusters for greater reliability.\n", " databricks_cluster_id=None, # Set None to create a new job cluster\n", " databricks_workspace_token_value=DATABRICKS_WORKSPACE_TOKEN_VALUE,\n", + " spark_config__databricks__work_dir=f\"dbfs:/{PROJECT_NAME}\",\n", " spark_config__databricks__workspace_instance_url=SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL,\n", " spark_config__databricks__config_template=json.dumps(databricks_config),\n", " feature_registry__api_endpoint=REGISTRY_ENDPOINT,\n", + " use_env_vars=False,\n", ")\n", "\n", "with open(config_path, \"r\") as f:\n", @@ -345,9 +349,10 @@ "metadata": {}, "outputs": [], "source": [ - "\n", + "if client.spark_runtime == \"local\":\n", + " data_source_path = data_filepath\n", "# If the notebook is running on Databricks, convert to spark path format\n", - "if client.spark_runtime == \"databricks\" and is_databricks():\n", + "elif client.spark_runtime == \"databricks\" and is_databricks():\n", " data_source_path = data_filepath.replace(\"/dbfs\", \"dbfs:\")\n", "# Otherwise, upload the local file to the cloud storage (either dbfs or adls).\n", "else:\n", @@ -754,13 +759,9 @@ "metadata": {}, "outputs": [], "source": [ - "if CLEAN_UP:\n", - " # Cleaning up the output files. CAUTION: this maybe dangerous if you \"reused\" the project name.\n", - " import shutil\n", - " if is_databricks():\n", - " shutil.rmtree(f\"/dbfs/{PROJECT_NAME}\", ignore_errors=False)\n", - " else:\n", - " shutil.rmtree(f\"{PROJECT_NAME}\", ignore_errors=False)" + "# Cleaning up the output files. CAUTION: this maybe dangerous if you \"reused\" the project name.\n", + "import shutil\n", + "shutil.rmtree(WORKING_DIR, ignore_errors=False)" ] } ], diff --git a/docs/samples/fraud_detection_demo.ipynb b/docs/samples/fraud_detection_demo.ipynb index 8e9fd0d4c..3d71a6ab0 100644 --- a/docs/samples/fraud_detection_demo.ipynb +++ b/docs/samples/fraud_detection_demo.ipynb @@ -102,6 +102,7 @@ "from feathr.datasets.utils import maybe_download\n", "from feathr.utils.config import generate_config\n", "from feathr.utils.job_utils import get_result_df\n", + "from feathr.utils.platform import is_databricks\n", "\n", "\n", "print(f\"Feathr version: {feathr.__version__}\")" @@ -298,10 +299,23 @@ "metadata": {}, "outputs": [], "source": [ - "# Download datasets TODO tmp folder for databricks?\n", - "account_info_file_path = str(Path(PROJECT_NAME, \"account_info.csv\"))\n", - "fraud_transactions_file_path = str(Path(PROJECT_NAME, \"fraud_transactions.csv\"))\n", - "obs_transactions_file_path = str(Path(PROJECT_NAME, \"obs_transactions.csv\"))\n", + "# Use dbfs if the notebook is running on Databricks\n", + "if is_databricks():\n", + " WORKING_DIR = f\"/dbfs/{PROJECT_NAME}\"\n", + "else:\n", + " WORKING_DIR = PROJECT_NAME" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Download datasets\n", + "account_info_file_path = f\"{WORKING_DIR}/account_info.csv\"\n", + "fraud_transactions_file_path = f\"{WORKING_DIR}/fraud_transactions.csv\"\n", + "obs_transactions_file_path = f\"{WORKING_DIR}/obs_transactions.csv\"\n", "maybe_download(\n", " src_url=FRAUD_DETECTION_ACCOUNT_INFO_URL,\n", " dst_filepath=account_info_file_path,\n", @@ -328,7 +342,7 @@ "obs_df = pd.read_csv(obs_transactions_file_path)\n", "obs_df[\"fraud_tag\"] = \"Unknown\"\n", "\n", - "transactions_file_path = str(Path(PROJECT_NAME, \"transactions.csv\"))\n", + "transactions_file_path = f\"{WORKING_DIR}/transactions.csv\"\n", "transactions_df = pd.concat([fraud_df, obs_df], ignore_index=True)\n", "transactions_df.to_csv(transactions_file_path, index=False)" ] @@ -340,11 +354,15 @@ "outputs": [], "source": [ "# Upload files to cloud if needed\n", - "if client.spark_runtime == \"local\" or (client.spark_runtime == \"databricks\" and is_databricks()):\n", + "if client.spark_runtime == \"local\":\n", " # In local mode, we can use the same data path as the source.\n", " # If the notebook is running on databricks, DATA_FILE_PATH should be already a dbfs path.\n", " account_info_source_path = account_info_file_path\n", " transactions_source_path = transactions_file_path\n", + "elif client.spark_runtime == \"databricks\" and is_databricks():\n", + " # If the notebook is running on databricks, we can use the same data path as the source.\n", + " account_info_source_path = account_info_file_path.replace(\"/dbfs\", \"dbfs:\")\n", + " transactions_source_path = transactions_file_path.replace(\"/dbfs\", \"dbfs:\")\n", "else:\n", " # Otherwise, upload the local file to the cloud storage (either dbfs or adls).\n", " account_info_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(account_info_file_path)\n", @@ -1064,7 +1082,7 @@ "metadata": {}, "outputs": [], "source": [ - "tsne = TSNE(n_components=2, perplexity=50, n_iter=300)\n", + "tsne = TSNE(n_components=2, perplexity=20, n_iter=300)\n", "tsne_results = tsne.fit_transform(features)" ] }, @@ -1177,6 +1195,25 @@ " import scrapbook as sb\n", " sb.glue(\"materialized_feature_values\", materialized_feature_values)" ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cleanup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Cleaning up the output files. CAUTION: this maybe dangerous if you \"reused\" the project name.\n", + "import shutil\n", + "shutil.rmtree(WORKING_DIR, ignore_errors=False)" + ] } ], "metadata": { diff --git a/docs/samples/nyc_taxi_demo.ipynb b/docs/samples/nyc_taxi_demo.ipynb index 90296b68d..768fce0f6 100644 --- a/docs/samples/nyc_taxi_demo.ipynb +++ b/docs/samples/nyc_taxi_demo.ipynb @@ -105,7 +105,6 @@ "from datetime import timedelta\n", "import os\n", "from pathlib import Path\n", - "from tempfile import TemporaryDirectory\n", "\n", "from pyspark.ml import Pipeline\n", "from pyspark.ml.evaluation import RegressionEvaluator\n", @@ -161,7 +160,7 @@ "outputs": [], "source": [ "RESOURCE_PREFIX = None # TODO fill the value used to deploy the resources via ARM template\n", - "PROJECT_NAME = \"feathr_getting_started\"\n", + "PROJECT_NAME = \"nyc_taxi\"\n", "\n", "# Currently support: 'azure_synapse', 'databricks', and 'local' \n", "SPARK_CLUSTER = \"local\"\n", @@ -176,9 +175,6 @@ "AZURE_SYNAPSE_URL = None # Set Azure Synapse workspace url to use Azure Synapse\n", "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", "\n", - "# Data store root path. Could be a local file system path, dbfs or Azure storage path like abfs or wasbs\n", - "DATA_STORE_PATH = TemporaryDirectory().name\n", - "\n", "# An existing Feathr config file path. If None, we'll generate a new config based on the constants in this cell.\n", "FEATHR_CONFIG_PATH = None\n", "\n", @@ -227,17 +223,6 @@ " os.environ[\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"] = DATABRICKS_WORKSPACE_TOKEN_VALUE" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Force to use dbfs if the notebook is running on Databricks\n", - "if is_databricks() and not DATA_STORE_PATH.startswith(\"dbfs:\"):\n", - " DATA_STORE_PATH = f\"dbfs:/{DATA_STORE_PATH.lstrip('/')}\"" - ] - }, { "cell_type": "code", "execution_count": null, @@ -397,6 +382,19 @@ "# Else, you must already have a spark session object available in databricks or synapse notebooks." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use dbfs if the notebook is running on Databricks\n", + "if is_databricks():\n", + " WORKING_DIR = f\"/dbfs/{PROJECT_NAME}\"\n", + "else:\n", + " WORKING_DIR = PROJECT_NAME" + ] + }, { "cell_type": "code", "execution_count": null, @@ -410,10 +408,9 @@ }, "outputs": [], "source": [ - "DATA_FILE_PATH = str(Path(DATA_STORE_PATH, \"nyc_taxi.csv\"))\n", - "\n", "# Download the data file\n", - "df_raw = nyc_taxi.get_spark_df(spark=spark, local_cache_path=DATA_FILE_PATH)\n", + "data_file_path = f\"{WORKING_DIR}/nyc_taxi_data.csv\"\n", + "df_raw = nyc_taxi.get_spark_df(spark=spark, local_cache_path=data_file_path)\n", "df_raw.limit(5).toPandas()" ] }, @@ -531,13 +528,15 @@ "outputs": [], "source": [ "# Upload files to cloud if needed\n", - "if client.spark_runtime == \"local\" or (client.spark_runtime == \"databricks\" and is_databricks()):\n", + "if client.spark_runtime == \"local\":\n", " # In local mode, we can use the same data path as the source.\n", - " # If the notebook is running on databricks, DATA_FILE_PATH should be already a dbfs path.\n", - " data_source_path = DATA_FILE_PATH\n", + " data_source_path = data_file_path\n", + "elif client.spark_runtime == \"databricks\" and is_databricks():\n", + " # If the notebook is running on databricks, we can use the same data path as the source.\n", + " data_source_path = data_file_path.replace(\"/dbfs\", \"dbfs:\")\n", "else:\n", " # Otherwise, upload the local file to the cloud storage (either dbfs or adls).\n", - " data_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(DATA_FILE_PATH) " + " data_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(data_file_path) " ] }, { @@ -1090,6 +1089,17 @@ " spark.stop()" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Cleaning up the output files. CAUTION: this maybe dangerous if you \"reused\" the project name.\n", + "import shutil\n", + "shutil.rmtree(WORKING_DIR, ignore_errors=False)" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/feathr_project/test/samples/test_notebooks.py b/feathr_project/test/samples/test_notebooks.py index 80947009a..6916d29de 100644 --- a/feathr_project/test/samples/test_notebooks.py +++ b/feathr_project/test/samples/test_notebooks.py @@ -1,5 +1,4 @@ from pathlib import Path -from tempfile import TemporaryDirectory import yaml import pytest @@ -29,7 +28,6 @@ def test__nyc_taxi_demo(config_path, tmp_path): notebook_name = "nyc_taxi_demo" - output_tmpdir = TemporaryDirectory() output_notebook_path = str(tmp_path.joinpath(f"{notebook_name}.ipynb")) print(f"Running {notebook_name} notebook as {output_notebook_path}") @@ -40,7 +38,6 @@ def test__nyc_taxi_demo(config_path, tmp_path): # kernel_name="python3", parameters=dict( FEATHR_CONFIG_PATH=config_path, - DATA_STORE_PATH=output_tmpdir.name, USE_CLI_AUTH=False, REGISTER_FEATURES=False, SCRAP_RESULTS=True, @@ -74,7 +71,6 @@ def test__feature_embedding(config_path, tmp_path): USE_CLI_AUTH=False, REGISTER_FEATURES=False, SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL=conf["spark_config"]["databricks"]["workspace_instance_url"], - CLEAN_UP=True, ), ) @@ -83,8 +79,7 @@ def test__feature_embedding(config_path, tmp_path): def test__fraud_detection_demo(config_path, tmp_path): notebook_name = "fraud_detection_demo" - output_tmpdir = TemporaryDirectory() - output_notebook_path = "output.ipynb" # TODO str(tmp_path.joinpath(f"{notebook_name}.ipynb")) + output_notebook_path = str(tmp_path.joinpath(f"{notebook_name}.ipynb")) print(f"Running {notebook_name} notebook as {output_notebook_path}") @@ -94,7 +89,6 @@ def test__fraud_detection_demo(config_path, tmp_path): # kernel_name="python3", parameters=dict( FEATHR_CONFIG_PATH=config_path, - DATA_STORE_PATH=output_tmpdir.name, # TODO we don't have this <-- USE_CLI_AUTH=False, SCRAP_RESULTS=True, ), From 2c0eac17927687b5a1720ddf46ec81a00590a234 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Tue, 13 Dec 2022 21:30:47 +0000 Subject: [PATCH 06/22] modify notebook test to go-around materialization issue Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/fraud_detection_demo.ipynb | 22 ++++++++++--------- feathr_project/test/samples/test_notebooks.py | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/samples/fraud_detection_demo.ipynb b/docs/samples/fraud_detection_demo.ipynb index 3d71a6ab0..343d012d0 100644 --- a/docs/samples/fraud_detection_demo.ipynb +++ b/docs/samples/fraud_detection_demo.ipynb @@ -210,7 +210,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": false + }, "outputs": [], "source": [ "# Redis password\n", @@ -231,7 +233,8 @@ "nuid": "50b2f73e-6380-42c3-91e8-4f3e15bc10d6", "showTitle": false, "title": "" - } + }, + "scrolled": true }, "outputs": [], "source": [ @@ -1132,8 +1135,8 @@ "ACCOUNT_FEATURE_TABLE_NAME = \"fraudDetectionAccountFeatures\" \n", "\n", "backfill_time = BackfillTime(\n", - " start=datetime(2013, 7, 31),\n", - " end=datetime(2013, 7, 31),\n", + " start=datetime(2013, 8, 4),\n", + " end=datetime(2013, 8, 4),\n", " step=timedelta(days=1),\n", ")" ] @@ -1149,7 +1152,7 @@ " ACCOUNT_FEATURE_TABLE_NAME,\n", " backfill_time=backfill_time,\n", " sinks=[RedisSink(table_name=ACCOUNT_FEATURE_TABLE_NAME)],\n", - " feature_names=account_feature_names,\n", + " feature_names=account_feature_names[1:],\n", " ),\n", " allow_materialize_non_agg_feature=True,\n", ")\n", @@ -1172,8 +1175,8 @@ "source": [ "materialized_feature_values = client.get_online_features(\n", " ACCOUNT_FEATURE_TABLE_NAME,\n", - " key=\"A1055520429343950\",\n", - " feature_names=account_feature_names,\n", + " key=\"A1055520452832600\",\n", + " feature_names=account_feature_names[1:],\n", ")\n", "materialized_feature_values" ] @@ -1197,7 +1200,6 @@ ] }, { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -1228,7 +1230,7 @@ "widgets": {} }, "kernelspec": { - "display_name": "feathr", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1242,7 +1244,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8 (main, Nov 24 2022, 14:13:03) [GCC 11.2.0]" + "version": "3.10.8" }, "vscode": { "interpreter": { diff --git a/feathr_project/test/samples/test_notebooks.py b/feathr_project/test/samples/test_notebooks.py index 6916d29de..c5e1f962b 100644 --- a/feathr_project/test/samples/test_notebooks.py +++ b/feathr_project/test/samples/test_notebooks.py @@ -98,4 +98,4 @@ def test__fraud_detection_demo(config_path, tmp_path): nb = sb.read_notebook(output_notebook_path) outputs = nb.scraps - assert outputs["materialized_feature_values"].data == pytest.approx(['GB', False, 0, 2000, 0.0, 0, 0, 0.0, 0.0], abs=1.) + assert outputs["materialized_feature_values"].data == pytest.approx([False, 0, 9, 239.0, 1, 1, 239.0, 33816.0], abs=1.) From 953155499c78a35c986c145dbd01eaa8ab607428 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Tue, 13 Dec 2022 16:47:00 -0800 Subject: [PATCH 07/22] Change notebook parameter name to align with client argument Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/feature_embedding.ipynb | 13 +++++++------ docs/samples/fraud_detection_demo.ipynb | 18 ++++++++++++------ docs/samples/nyc_taxi_demo.ipynb | 14 ++++++++++---- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/docs/samples/feature_embedding.ipynb b/docs/samples/feature_embedding.ipynb index 37a0a5041..b1e5f8ff1 100755 --- a/docs/samples/feature_embedding.ipynb +++ b/docs/samples/feature_embedding.ipynb @@ -107,15 +107,16 @@ "\n", "REGISTRY_ENDPOINT = f\"https://{RESOURCE_PREFIX}webapp.azurewebsites.net/api/v1\"\n", "\n", + "# TODO fill values to use databricks cluster:\n", + "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", "if is_databricks():\n", " # If this notebook is running on Databricks, its context can be used to retrieve token and instance URL\n", " ctx = dbutils.notebook.entry_point.getDbutils().notebook().getContext()\n", " DATABRICKS_WORKSPACE_TOKEN_VALUE = ctx.apiToken().get()\n", " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = f\"https://{ctx.tags().get('browserHostName').get()}\"\n", "else:\n", - " # TODO fill the values.\n", - " DATABRICKS_WORKSPACE_TOKEN_VALUE = None\n", - " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = None\n", + " DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", + " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = None # Set Databricks workspace url to use databricks\n", "\n", "# We'll need an authentication credential to access Azure resources and register features \n", "USE_CLI_AUTH = False # Set True to use interactive authentication\n", @@ -778,7 +779,7 @@ "widgets": {} }, "kernelspec": { - "display_name": "Python 3.10.4 ('feathr')", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -792,11 +793,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8 (main, Nov 24 2022, 14:13:03) [GCC 11.2.0]" + "version": "3.8.10 (default, Nov 14 2022, 12:59:47) \n[GCC 9.4.0]" }, "vscode": { "interpreter": { - "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" + "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" } } }, diff --git a/docs/samples/fraud_detection_demo.ipynb b/docs/samples/fraud_detection_demo.ipynb index 343d012d0..ddcb7bc21 100644 --- a/docs/samples/fraud_detection_demo.ipynb +++ b/docs/samples/fraud_detection_demo.ipynb @@ -126,8 +126,14 @@ "\n", "# TODO fill values to use databricks cluster:\n", "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", - "DATABRICKS_URL = None # Set Databricks workspace url to use databricks\n", - "DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", + "if is_databricks():\n", + " # If this notebook is running on Databricks, its context can be used to retrieve token and instance URL\n", + " ctx = dbutils.notebook.entry_point.getDbutils().notebook().getContext()\n", + " DATABRICKS_WORKSPACE_TOKEN_VALUE = ctx.apiToken().get()\n", + " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = f\"https://{ctx.tags().get('browserHostName').get()}\"\n", + "else:\n", + " DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", + " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = None # Set Databricks workspace url to use databricks\n", "\n", "# TODO fill values to use Azure Synapse cluster:\n", "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", @@ -247,7 +253,7 @@ " spark_config__spark_cluster=SPARK_CLUSTER,\n", " spark_config__azure_synapse__dev_url=AZURE_SYNAPSE_URL,\n", " spark_config__azure_synapse__pool_name=AZURE_SYNAPSE_SPARK_POOL,\n", - " spark_config__databricks__workspace_instance_url=DATABRICKS_URL,\n", + " spark_config__databricks__workspace_instance_url=SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL,\n", " databricks_cluster_id=DATABRICKS_CLUSTER_ID,\n", " )\n", "\n", @@ -1230,7 +1236,7 @@ "widgets": {} }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "feathr", "language": "python", "name": "python3" }, @@ -1244,11 +1250,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.8 (main, Nov 24 2022, 14:13:03) [GCC 11.2.0]" }, "vscode": { "interpreter": { - "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" + "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" } } }, diff --git a/docs/samples/nyc_taxi_demo.ipynb b/docs/samples/nyc_taxi_demo.ipynb index 768fce0f6..fc553f422 100644 --- a/docs/samples/nyc_taxi_demo.ipynb +++ b/docs/samples/nyc_taxi_demo.ipynb @@ -167,8 +167,14 @@ "\n", "# TODO fill values to use databricks cluster:\n", "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", - "DATABRICKS_URL = None # Set Databricks workspace url to use databricks\n", - "DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", + "if is_databricks():\n", + " # If this notebook is running on Databricks, its context can be used to retrieve token and instance URL\n", + " ctx = dbutils.notebook.entry_point.getDbutils().notebook().getContext()\n", + " DATABRICKS_WORKSPACE_TOKEN_VALUE = ctx.apiToken().get()\n", + " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = f\"https://{ctx.tags().get('browserHostName').get()}\"\n", + "else:\n", + " DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", + " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = None # Set Databricks workspace url to use databricks\n", "\n", "# TODO fill values to use Azure Synapse cluster:\n", "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", @@ -301,7 +307,7 @@ " spark_config__spark_cluster=SPARK_CLUSTER,\n", " spark_config__azure_synapse__dev_url=AZURE_SYNAPSE_URL,\n", " spark_config__azure_synapse__pool_name=AZURE_SYNAPSE_SPARK_POOL,\n", - " spark_config__databricks__workspace_instance_url=DATABRICKS_URL,\n", + " spark_config__databricks__workspace_instance_url=SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL,\n", " databricks_cluster_id=DATABRICKS_CLUSTER_ID,\n", " )\n", "\n", @@ -1153,7 +1159,7 @@ }, "vscode": { "interpreter": { - "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" + "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" } } }, From 0c3d7ab1fe7edee6605b11e265cab1d7a5b58a41 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Thu, 15 Dec 2022 07:57:02 -0800 Subject: [PATCH 08/22] Update recommendation notebook Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/feature_embedding.ipynb | 20 +- ...product_recommendation_demo_advanced.ipynb | 855 +++++++++--------- feathr_project/feathr/datasets/constants.py | 28 + 3 files changed, 468 insertions(+), 435 deletions(-) diff --git a/docs/samples/feature_embedding.ipynb b/docs/samples/feature_embedding.ipynb index b1e5f8ff1..9868e0879 100755 --- a/docs/samples/feature_embedding.ipynb +++ b/docs/samples/feature_embedding.ipynb @@ -70,6 +70,7 @@ " # feathr_configurations\n", " SparkExecutionConfiguration,\n", ")\n", + "from feathr.datasets.constants import HOTEL_REVIEWS_URL\n", "from feathr.datasets.utils import maybe_download\n", "from feathr.utils.config import DEFAULT_DATABRICKS_CLUSTER_CONFIG, generate_config\n", "from feathr.utils.job_utils import get_result_df\n", @@ -176,23 +177,6 @@ "First, prepare the hotel review dataset." ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "8a4bceb6-2d39-4267-93a2-84158d605e51", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "DATA_URL = \"https://azurefeathrstorage.blob.core.windows.net/public/sample_data/hotel_reviews_100_with_id.csv\"" - ] - }, { "cell_type": "code", "execution_count": null, @@ -221,7 +205,7 @@ "outputs": [], "source": [ "data_filepath = f\"{WORKING_DIR}/hotel_reviews_100_with_id.csv\"\n", - "maybe_download(src_url=DATA_URL, dst_filepath=data_filepath)" + "maybe_download(src_url=HOTEL_REVIEWS_URL, dst_filepath=data_filepath)" ] }, { diff --git a/docs/samples/product_recommendation_demo_advanced.ipynb b/docs/samples/product_recommendation_demo_advanced.ipynb index aafbdf0f0..60c2b3b1b 100644 --- a/docs/samples/product_recommendation_demo_advanced.ipynb +++ b/docs/samples/product_recommendation_demo_advanced.ipynb @@ -1,6 +1,7 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -16,7 +17,7 @@ "This notebook illustrates the use of Feathr Feature Store to create a model that predict users' rating for different products for a e-commerce website.\n", "\n", "## Model Problem Statement\n", - "The e-commerce website has collected past user ratings for various products. The websie also collected data about user and product, like user age, product category etc. Now we want to predict users' product rating for new product so that we can recommend the new product to users that give a high rating for those products.\n", + "The e-commerce website has collected past user ratings for various products. The website also collected data about user and product, like user age, product category etc. Now we want to predict users' product rating for new product so that we can recommend the new product to users that give a high rating for those products.\n", "\n", "After the model is trained, given a user_id, product_id pair and features, we should be able to predict the product rating that the user will give for this product_id.\n", "\n", @@ -56,6 +57,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -66,57 +68,29 @@ } }, "source": [ - "## Prerequisite: Install Feathr \n", + "## Prerequisite: Install Feathr pip package and notebook dependencies\n", "\n", - "Install Feathr using pip:\n", - "\n", - "`pip install -U feathr pandavro scikit-learn`" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "ab5d219b-b827-4f25-9918-d7cb7b47938e", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Prerequisite: Configure the required environment with Feathr Quick Start Template\n", - "\n", - "In the first step (Provision cloud resources), you should have provisioned all the required cloud resources. Run the code below to install Feathr, login to Azure to get the required credentials to access more cloud resources." + "In the first step (Provision cloud resources), you should have provisioned all the required cloud resources. Run the code below to install Feathr" ] }, { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "146a1443-ce8b-4b8e-8169-2417af8bcb62", - "showTitle": false, - "title": "" - } - }, + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ - "**REQUIRED STEP: Fill in the resource prefix when provisioning the resources**" + "# Install feathr from the latest codes in the repo. You may use `pip install feathr[notebook]` as well.\n", + "# !pip install \"git+https://github.com/feathr-ai/feathr.git#subdirectory=feathr_project&egg=feathr[notebook]\" " ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "99b2d855-dae1-4ac8-8492-406dad242326", - "showTitle": false, - "title": "" - } - }, + "metadata": {}, "outputs": [], "source": [ - "resource_prefix = \"feathr_resource_prefix\"" + "%load_ext autoreload\n", + "%autoreload 2" ] }, { @@ -125,108 +99,133 @@ "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, - "nuid": "95ad2a97-b8e7-4189-8463-51fe419d29c5", + "nuid": "0f3135eb-15c5-4f46-90ff-881a21cc59df", "showTitle": false, "title": "" } }, "outputs": [], "source": [ - "! pip install feathr azure-cli pandavro scikit-learn\n" + "import glob\n", + "import os\n", + "import tempfile\n", + "from datetime import datetime, timedelta\n", + "from math import sqrt\n", + "\n", + "import pandas as pd\n", + "from pyspark.sql import DataFrame\n", + "\n", + "\n", + "import feathr\n", + "from feathr import (\n", + " FeathrClient,\n", + " BOOLEAN, FLOAT, INT32, ValueType,\n", + " Feature, DerivedFeature, FeatureAnchor,\n", + " BackfillTime, MaterializationSettings,\n", + " FeatureQuery, ObservationSettings,\n", + " RedisSink,\n", + " INPUT_CONTEXT, HdfsSource,\n", + " WindowAggTransformation,\n", + " TypedKey,\n", + ")\n", + "from feathr.datasets.constants import (\n", + " PRODUCT_RECOMMENDATION_USER_OBSERVATION_URL,\n", + " PRODUCT_RECOMMENDATION_USER_PROFILE_URL,\n", + " PRODUCT_RECOMMENDATION_USER_PURCHASE_HISTORY_URL,\n", + " PRODUCT_RECOMMENDATION_PRODUCT_DETAIL_URL,\n", + ")\n", + "from feathr.datasets.utils import maybe_download\n", + "from feathr.utils.config import generate_config\n", + "from feathr.utils.job_utils import get_result_df\n", + "from feathr.utils.platform import is_databricks\n", + "\n", + "\n", + "print(f\"Feathr version: {feathr.__version__}\")" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "When running this notebook in synapse, you may get some errors or blocks installing above packages in one cell. Suggest to try installing them in seperate cells if meet some issues. Eg. ! pip install feathr, ! pip install azure-cli , ! pip install pandavro, ! pip install scikit-learn" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "a4d7dc6a-d753-4fb6-9683-2766f9a046c7", - "showTitle": false, - "title": "" - } - }, - "source": [ - "Login to Azure with a device code (You will see instructions in the output):" + "> If you meet errors like 'cannot import FeatherClient from feathr', it may be caused by incompatible version of 'aiohttp'. Please try to install/upgrade it by running: '! pip install -U aiohttp' or '! pip install aiohttp==3.8.3'" ] }, { "cell_type": "code", "execution_count": null, "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "42cf1691-b8de-48d2-b174-0c269950d470", - "showTitle": false, - "title": "" - } + "tags": [ + "parameters" + ] }, "outputs": [], "source": [ - "! az login --use-device-code" + "RESOURCE_PREFIX = \"\" # TODO fill the value used to deploy the resources via ARM template\n", + "PROJECT_NAME = \"product_recommendation\"\n", + "\n", + "# Currently support: 'azure_synapse', 'databricks', and 'local' \n", + "SPARK_CLUSTER = \"local\"\n", + "\n", + "# TODO fill values to use databricks cluster:\n", + "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", + "if is_databricks():\n", + " # If this notebook is running on Databricks, its context can be used to retrieve token and instance URL\n", + " ctx = dbutils.notebook.entry_point.getDbutils().notebook().getContext()\n", + " DATABRICKS_WORKSPACE_TOKEN_VALUE = ctx.apiToken().get()\n", + " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = f\"https://{ctx.tags().get('browserHostName').get()}\"\n", + "else:\n", + " DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", + " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = None # Set Databricks workspace url to use databricks\n", + "\n", + "# TODO fill values to use Azure Synapse cluster:\n", + "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", + "AZURE_SYNAPSE_URL = None # Set Azure Synapse workspace url to use Azure Synapse\n", + "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", + "\n", + "# An existing Feathr config file path. If None, we'll generate a new config based on the constants in this cell.\n", + "FEATHR_CONFIG_PATH = None\n", + "\n", + "USE_CLI_AUTH = False # Set to True to use CLI authentication\n", + "\n", + "# If set True, register the features to Feathr registry.\n", + "REGISTER_FEATURES = False\n", + "\n", + "# (For the notebook test pipeline) If true, use ScrapBook package to collect the results.\n", + "SCRAP_RESULTS = False" ] }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "0f3135eb-15c5-4f46-90ff-881a21cc59df", - "showTitle": false, - "title": "" - } - }, - "outputs": [], + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, "source": [ - "import glob\n", - "import os\n", - "import tempfile\n", - "from datetime import datetime, timedelta\n", - "from math import sqrt\n", + "## Setup necessary environment variables (Skip if using the above Quick Start Template)\n", "\n", - "import pandas as pd\n", - "import pandavro as pdx\n", - "from feathr import FeathrClient\n", - "from feathr import BOOLEAN, FLOAT, INT32, ValueType\n", - "from feathr import Feature, DerivedFeature, FeatureAnchor\n", - "from feathr import BackfillTime, MaterializationSettings\n", - "from feathr import FeatureQuery, ObservationSettings\n", - "from feathr import RedisSink\n", - "from feathr import INPUT_CONTEXT, HdfsSource\n", - "from feathr import WindowAggTransformation\n", - "from feathr import TypedKey\n", - "from sklearn.metrics import mean_squared_error\n", - "from sklearn.model_selection import train_test_split\n", - "from azure.identity import DefaultAzureCredential\n", - "from azure.keyvault.secrets import SecretClient\n" + "You should setup the environment variables in order to run this sample. More environment variables can be set by referring to [feathr_config.yaml](https://github.com/feathr-ai/feathr/blob/main/feathr_project/feathrcli/data/feathr_user_workspace/feathr_config.yaml) and use that as the source of truth. It also has more explanations on the meaning of each variable.\n", + "\n", + "To run this notebook, for Azure users, you need REDIS_PASSWORD.\n", + "To run this notebook, for Databricks useres, you need DATABRICKS_WORKSPACE_TOKEN_VALUE and REDIS_PASSWORD." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "If you meet errors like 'cannot import FeatherClient from feathr', it may be caused by incompatible version of 'aiohttp'. Please try to install/upgrade it by running: '! pip install -U aiohttp' or '! pip install aiohttp==3.8.3'" + "if SPARK_CLUSTER == \"azure_synapse\" and not os.environ.get(\"ADLS_KEY\"):\n", + " os.environ[\"ADLS_KEY\"] = ADLS_KEY\n", + "elif SPARK_CLUSTER == \"databricks\" and not os.environ.get(\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"):\n", + " os.environ[\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"] = DATABRICKS_WORKSPACE_TOKEN_VALUE" ] }, { + "attachments": {}, "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "a58b69e8-fbd2-48dd-81cb-85163dfbb676", - "showTitle": false, - "title": "" - } - }, + "metadata": {}, "source": [ - "**Permission**\n", + "## Permission\n", "\n", "To proceed with the following steps, you may need additional permission: permission to access the keyvault, permission to access the Storage Blob as a Contributor and permission to submit jobs to Synapse cluster. Skip this step if you have already given yourself the access. Otherwise, run the following lines of command in the Cloud Shell before running the cell below.\n", "\n", @@ -248,65 +247,43 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "510120a8-d456-4aa1-9b0b-6e10bd774b78", - "showTitle": false, - "title": "" - } - }, "source": [ - "**Get all the required credentials from Azure KeyVault**" + "# Get an authentication credential to access Azure resources and register features\n", + "if USE_CLI_AUTH:\n", + " # Use AZ CLI interactive browser authentication\n", + " !az login --use-device-code\n", + " from azure.identity import AzureCliCredential\n", + " credential = AzureCliCredential(additionally_allowed_tenants=['*'],)\n", + "elif \"AZURE_TENANT_ID\" in os.environ and \"AZURE_CLIENT_ID\" in os.environ and \"AZURE_CLIENT_SECRET\" in os.environ:\n", + " # Use Environment variable secret\n", + " from azure.identity import EnvironmentCredential\n", + " credential = EnvironmentCredential()\n", + "else:\n", + " # Try to use the default credential\n", + " from azure.identity import DefaultAzureCredential\n", + " credential = DefaultAzureCredential(\n", + " exclude_interactive_browser_credential=False,\n", + " additionally_allowed_tenants=['*'],\n", + " )" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "b589fc31-11f9-4bea-963a-9dab88cd6689", - "showTitle": true, - "title": "" - } - }, + "metadata": {}, "outputs": [], "source": [ - "# Get all the required credentials from Azure Key Vault\n", - "key_vault_name=resource_prefix+\"kv\"\n", - "synapse_workspace_url=resource_prefix+\"syws\"\n", - "adls_account=resource_prefix+\"dls\"\n", - "adls_fs_name=resource_prefix+\"fs\"\n", - "purview_name=resource_prefix+\"purview\"\n", - "key_vault_uri = f\"https://{key_vault_name}.vault.azure.net\"\n", - "credential = DefaultAzureCredential(exclude_interactive_browser_credential=False, additionally_allowed_tenants=['*'])\n", - "client = SecretClient(vault_url=key_vault_uri, credential=credential)\n", - "secretName = \"FEATHR-ONLINE-STORE-CONN\"\n", - "retrieved_secret = client.get_secret(secretName).value\n", - "\n", - "# Get redis credentials; This is to parse Redis connection string.\n", - "redis_port=retrieved_secret.split(',')[0].split(\":\")[1]\n", - "redis_host=retrieved_secret.split(',')[0].split(\":\")[0]\n", - "redis_password=retrieved_secret.split(',')[1].split(\"password=\",1)[1]\n", - "redis_ssl=retrieved_secret.split(',')[2].split(\"ssl=\",1)[1]\n", - "\n", - "# Set the resource link\n", - "os.environ['spark_config__azure_synapse__dev_url'] = f'https://{synapse_workspace_url}.dev.azuresynapse.net'\n", - "os.environ['spark_config__azure_synapse__pool_name'] = 'spark31'\n", - "os.environ['spark_config__azure_synapse__workspace_dir'] = f'abfss://{adls_fs_name}@{adls_account}.dfs.core.windows.net/feathr_project'\n", - "os.environ['online_store__redis__host'] = redis_host\n", - "os.environ['online_store__redis__port'] = redis_port\n", - "os.environ['online_store__redis__ssl_enabled'] = redis_ssl\n", - "os.environ['REDIS_PASSWORD']=redis_password\n", - "feathr_output_path = f'abfss://{adls_fs_name}@{adls_account}.dfs.core.windows.net/feathr_output'" + "# Redis password\n", + "if 'REDIS_PASSWORD' not in os.environ:\n", + " from azure.keyvault.secrets import SecretClient\n", + " vault_url = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", + " secret_client = SecretClient(vault_url=vault_url, credential=credential)\n", + " retrieved_secret = secret_client.get_secret('FEATHR-ONLINE-STORE-CONN').value\n", + " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -321,7 +298,7 @@ "\n", "In the first step (Provision cloud resources), you should have provisioned all the required cloud resources. If you use Feathr CLI to create a workspace, you should have a folder with a file called `feathr_config.yaml` in it with all the required configurations. Otherwise, update the configuration below.\n", "\n", - "The code below will write this configuration string to a temporary location and load it to Feathr. Please still refer to [feathr_config.yaml](https://github.com/feathr-ai/feathr/blob/main/feathr_project/feathrcli/data/feathr_user_workspace/feathr_config.yaml) and use that as the source of truth. It should also have more explanations on the meaning of each variable." + "The code below will write this configuration string to a temporary location so that Feathr client can load it. Please refer to [feathr_config.yaml](https://github.com/feathr-ai/feathr/blob/main/feathr_project/feathrcli/data/feathr_user_workspace/feathr_config.yaml) for more details." ] }, { @@ -337,56 +314,21 @@ }, "outputs": [], "source": [ - "import tempfile\n", - "yaml_config = \"\"\"\n", - "# Please refer to https://github.com/feathr-ai/feathr/blob/main/feathr_project/feathrcli/data/feathr_user_workspace/feathr_config.yaml for explanations on the meaning of each field.\n", - "api_version: 1\n", - "project_config:\n", - " project_name: 'feathr_getting_started'\n", - " required_environment_variables:\n", - " - 'REDIS_PASSWORD'\n", - "offline_store:\n", - " adls:\n", - " adls_enabled: true\n", - " wasb:\n", - " wasb_enabled: true\n", - " s3:\n", - " s3_enabled: false\n", - " s3_endpoint: 's3.amazonaws.com'\n", - " jdbc:\n", - " jdbc_enabled: false\n", - " jdbc_database: ''\n", - " jdbc_table: ''\n", - " snowflake:\n", - " snowflake_enabled: false\n", - " url: \".snowflakecomputing.com\"\n", - " user: \"\"\n", - " role: \"\"\n", - " warehouse: \"\"\n", - "spark_config:\n", - " spark_cluster: 'azure_synapse'\n", - " spark_result_output_parts: '1'\n", - " azure_synapse:\n", - " dev_url: 'https://.dev.azuresynapse.net'\n", - " pool_name: 'spark3'\n", - " workspace_dir: 'abfss://{adls_fs_name}@{adls_account}.dfs.core.windows.net/feathr_getting_started'\n", - " executor_size: 'Small'\n", - " executor_num: 1\n", - " databricks:\n", - " workspace_instance_url: 'https://.azuredatabricks.net'\n", - " config_template: {'run_name':'','new_cluster':{'spark_version':'9.1.x-scala2.12','node_type_id':'Standard_D3_v2','num_workers':2,'spark_conf':{}},'libraries':[{'jar':''}],'spark_jar_task':{'main_class_name':'','parameters':['']}}\n", - " work_dir: 'dbfs:/feathr_getting_started'\n", - "online_store:\n", - " redis:\n", - " host: '.redis.cache.windows.net'\n", - " port: 6380\n", - " ssl_enabled: True\n", - "feature_registry:\n", - " api_endpoint: \"https://.azurewebsites.net/api/v1\"\n", - "\"\"\"\n", - "tmp = tempfile.NamedTemporaryFile(mode='w', delete=False)\n", - "with open(tmp.name, \"w\") as text_file:\n", - " text_file.write(yaml_config)\n" + "if FEATHR_CONFIG_PATH:\n", + " config_path = FEATHR_CONFIG_PATH\n", + "else:\n", + " config_path = generate_config(\n", + " resource_prefix=RESOURCE_PREFIX,\n", + " project_name=PROJECT_NAME,\n", + " spark_config__spark_cluster=SPARK_CLUSTER,\n", + " spark_config__azure_synapse__dev_url=AZURE_SYNAPSE_URL,\n", + " spark_config__azure_synapse__pool_name=AZURE_SYNAPSE_SPARK_POOL,\n", + " spark_config__databricks__workspace_instance_url=SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL,\n", + " databricks_cluster_id=DATABRICKS_CLUSTER_ID,\n", + " )\n", + "\n", + "with open(config_path, 'r') as f: \n", + " print(f.read())" ] }, { @@ -394,51 +336,114 @@ "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, - "nuid": "91548af7-5d87-4743-9db4-8fac7ba67804", + "nuid": "794492ed-66b0-4787-adc6-3f234c4739a9", "showTitle": false, "title": "" } }, "source": [ - "## Setup necessary environment variables (Skip if using the above Quick Start Template)\n", - "\n", - "You should setup the environment variables in order to run this sample. More environment variables can be set by referring to [feathr_config.yaml](https://github.com/feathr-ai/feathr/blob/main/feathr_project/feathrcli/data/feathr_user_workspace/feathr_config.yaml) and use that as the source of truth. It also has more explanations on the meaning of each variable.\n", - "\n", - "To run this notebook, for Azure users, you need REDIS_PASSWORD.\n", - "To run this notebook, for Databricks useres, you need DATABRICKS_WORKSPACE_TOKEN_VALUE and REDIS_PASSWORD." + "# Initialize Feathr Client" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": { "application/vnd.databricks.v1+cell": { "inputWidgets": {}, - "nuid": "794492ed-66b0-4787-adc6-3f234c4739a9", + "nuid": "0c748f9d-210b-4c1d-a414-b30328d5e219", "showTitle": false, "title": "" } }, + "outputs": [], "source": [ - "# Initialize Feathr Client" + "client = FeathrClient(config_path=config_path, credential=credential)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prepare Datasets\n", + "\n", + "1. Download datasets\n", + "2. Upload to cloud if necessary so that the target cluster can consume as the data source" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "0c748f9d-210b-4c1d-a414-b30328d5e219", - "showTitle": false, - "title": "" - } - }, + "metadata": {}, + "outputs": [], + "source": [ + "# Use dbfs if the notebook is running on Databricks\n", + "if is_databricks():\n", + " WORKING_DIR = f\"/dbfs/{PROJECT_NAME}\"\n", + "else:\n", + " WORKING_DIR = PROJECT_NAME" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Download datasets\n", + "user_observation_file_path = f\"{WORKING_DIR}/user_observation.csv\"\n", + "user_profile_file_path = f\"{WORKING_DIR}/user_profile.csv\"\n", + "user_purchase_history_file_path = f\"{WORKING_DIR}/user_purchase_history.csv\"\n", + "product_detail_file_path = f\"{WORKING_DIR}/product_detail.csv\"\n", + "maybe_download(\n", + " src_url=PRODUCT_RECOMMENDATION_USER_OBSERVATION_URL,\n", + " dst_filepath=user_observation_file_path,\n", + ")\n", + "maybe_download(\n", + " src_url=PRODUCT_RECOMMENDATION_USER_PROFILE_URL,\n", + " dst_filepath=user_profile_file_path,\n", + ")\n", + "maybe_download(\n", + " src_url=PRODUCT_RECOMMENDATION_USER_PURCHASE_HISTORY_URL,\n", + " dst_filepath=user_purchase_history_file_path,\n", + ")\n", + "maybe_download(\n", + " src_url=PRODUCT_RECOMMENDATION_PRODUCT_DETAIL_URL,\n", + " dst_filepath=product_detail_file_path,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ - "client = FeathrClient(config_path=tmp.name, credential=credential)" + "# Upload files to cloud if needed\n", + "if client.spark_runtime == \"local\":\n", + " # In local mode, we can use the same data path as the source.\n", + " # If the notebook is running on databricks, DATA_FILE_PATH should be already a dbfs path.\n", + " user_observation_source_path = user_observation_file_path\n", + " user_profile_source_path = user_profile_file_path\n", + " user_purchase_history_source_path = user_purchase_history_file_path\n", + " product_detail_source_path = product_detail_file_path\n", + "elif client.spark_runtime == \"databricks\" and is_databricks():\n", + " # If the notebook is running on databricks, we can use the same data path as the source.\n", + " user_observation_source_path = user_observation_file_path.replace(\"/dbfs\", \"dbfs:\")\n", + " user_profile_source_path = user_profile_file_path.replace(\"/dbfs\", \"dbfs:\")\n", + " user_purchase_history_source_path = user_purchase_history_file_path.replace(\"/dbfs\", \"dbfs:\")\n", + " product_detail_source_path = product_detail_file_path.replace(\"/dbfs\", \"dbfs:\")\n", + "else:\n", + " # Otherwise, upload the local file to the cloud storage (either dbfs or adls).\n", + " user_observation_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(user_observation_file_path)\n", + " user_profile_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(user_profile_file_path)\n", + " user_purchase_history_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(user_purchase_history_file_path)\n", + " product_detail_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(product_detail_file_path)" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -450,7 +455,7 @@ }, "source": [ "## Explore the raw source data\n", - "We have 4 datasets to work with: one observation dataset(a.k.a. label dataset), two raw datasets to generate features for users, one raw datasets to generate features for product." + "We have 4 datasets to work with: one observation dataset (a.k.a. label dataset), two raw datasets to generate features for users, one raw datasets to generate features for product." ] }, { @@ -469,8 +474,7 @@ "# Observation dataset(a.k.a. label dataset)\n", "# Observation dataset usually comes with a event_timestamp to denote when the observation happened.\n", "# The label here is product_rating. Our model objective is to predict a user's rating for this product.\n", - "import pandas as pd\n", - "pd.read_csv(\"https://azurefeathrstorage.blob.core.windows.net/public/sample_data/product_recommendation_sample/user_observation_mock_data.csv\")" + "pd.read_csv(user_observation_file_path).head()" ] }, { @@ -488,8 +492,7 @@ "source": [ "# User profile dataset\n", "# Used to generate user features\n", - "import pandas as pd\n", - "pd.read_csv(\"https://azurefeathrstorage.blob.core.windows.net/public/sample_data/product_recommendation_sample/user_profile_mock_data.csv\")" + "pd.read_csv(user_profile_file_path).head()" ] }, { @@ -507,8 +510,7 @@ "source": [ "# User purchase history dataset.\n", "# Used to generate user features. This is activity type data, so we need to use aggregation to genearte features.\n", - "import pandas as pd\n", - "pd.read_csv(\"https://azurefeathrstorage.blob.core.windows.net/public/sample_data/product_recommendation_sample/user_purchase_history_mock_data.csv\")" + "pd.read_csv(user_purchase_history_file_path).head()" ] }, { @@ -526,8 +528,7 @@ "source": [ "# Product detail dataset.\n", "# Used to generate product features.\n", - "import pandas as pd\n", - "pd.read_csv(\"https://azurefeathrstorage.blob.core.windows.net/public/sample_data/product_recommendation_sample/product_detail_mock_data.csv\")" + "pd.read_csv(product_detail_file_path).head()" ] }, { @@ -602,16 +603,18 @@ }, "outputs": [], "source": [ - "from pyspark.sql import SparkSession, DataFrame\n", "def feathr_udf_preprocessing(df: DataFrame) -> DataFrame:\n", " from pyspark.sql.functions import col\n", - " df = df.withColumn(\"tax_rate_decimal\", col(\"tax_rate\")/100)\n", - " df.show(10)\n", + "\n", + " df = df.withColumn(\"tax_rate_decimal\", col(\"tax_rate\") / 100)\n", " return df\n", "\n", - "batch_source = HdfsSource(name=\"userProfileData\",\n", - " path=\"wasbs://public@azurefeathrstorage.blob.core.windows.net/sample_data/product_recommendation_sample/user_profile_mock_data.csv\",\n", - " preprocessing=feathr_udf_preprocessing)" + "\n", + "batch_source = HdfsSource(\n", + " name=\"userProfileData\",\n", + " path=user_profile_source_path,\n", + " preprocessing=feathr_udf_preprocessing,\n", + ")" ] }, { @@ -628,37 +631,48 @@ "outputs": [], "source": [ "# Let's define some features for users so our recommendation can be customized for users.\n", - "user_id = TypedKey(key_column=\"user_id\",\n", - " key_column_type=ValueType.INT32,\n", - " description=\"user id\",\n", - " full_name=\"product_recommendation.user_id\")\n", - "\n", - "feature_user_age = Feature(name=\"feature_user_age\",\n", - " key=user_id,\n", - " feature_type=INT32, transform=\"age\")\n", - "feature_user_tax_rate = Feature(name=\"feature_user_tax_rate\",\n", - " key=user_id,\n", - " feature_type=FLOAT,\n", - " transform=\"tax_rate_decimal\")\n", - "feature_user_gift_card_balance = Feature(name=\"feature_user_gift_card_balance\",\n", - " key=user_id,\n", - " feature_type=FLOAT,\n", - " transform=\"gift_card_balance\")\n", - "feature_user_has_valid_credit_card = Feature(name=\"feature_user_has_valid_credit_card\",\n", - " key=user_id,\n", - " feature_type=BOOLEAN,\n", - " transform=\"number_of_credit_cards > 0\")\n", - " \n", + "user_id = TypedKey(\n", + " key_column=\"user_id\",\n", + " key_column_type=ValueType.INT32,\n", + " description=\"user id\",\n", + " full_name=\"product_recommendation.user_id\",\n", + ")\n", + "\n", + "feature_user_age = Feature(\n", + " name=\"feature_user_age\",\n", + " key=user_id,\n", + " feature_type=INT32,\n", + " transform=\"age\",\n", + ")\n", + "feature_user_tax_rate = Feature(\n", + " name=\"feature_user_tax_rate\",\n", + " key=user_id,\n", + " feature_type=FLOAT,\n", + " transform=\"tax_rate_decimal\",\n", + ")\n", + "feature_user_gift_card_balance = Feature(\n", + " name=\"feature_user_gift_card_balance\",\n", + " key=user_id,\n", + " feature_type=FLOAT,\n", + " transform=\"gift_card_balance\",\n", + ")\n", + "feature_user_has_valid_credit_card = Feature(\n", + " name=\"feature_user_has_valid_credit_card\",\n", + " key=user_id,\n", + " feature_type=BOOLEAN,\n", + " transform=\"number_of_credit_cards > 0\",\n", + ")\n", + "\n", "features = [\n", " feature_user_age,\n", " feature_user_tax_rate,\n", " feature_user_gift_card_balance,\n", - " feature_user_has_valid_credit_card\n", + " feature_user_has_valid_credit_card,\n", "]\n", "\n", - "user_feature_anchor = FeatureAnchor(name=\"anchored_features\",\n", - " source=batch_source,\n", - " features=features)" + "user_feature_anchor = FeatureAnchor(\n", + " name=\"anchored_features\", source=batch_source, features=features\n", + ")" ] }, { @@ -675,31 +689,35 @@ "outputs": [], "source": [ "# Let's define some features for the products so our recommendation can be customized for proudcts.\n", - "product_batch_source = HdfsSource(name=\"productProfileData\",\n", - " path=\"wasbs://public@azurefeathrstorage.blob.core.windows.net/sample_data/product_recommendation_sample/product_detail_mock_data.csv\")\n", - "\n", - "product_id = TypedKey(key_column=\"product_id\",\n", - " key_column_type=ValueType.INT32,\n", - " description=\"product id\",\n", - " full_name=\"product_recommendation.product_id\")\n", - "\n", - "feature_product_quantity = Feature(name=\"feature_product_quantity\",\n", - " key=product_id,\n", - " feature_type=FLOAT, \n", - " transform=\"quantity\")\n", - "feature_product_price = Feature(name=\"feature_product_price\",\n", - " key=product_id,\n", - " feature_type=FLOAT,\n", - " transform=\"price\")\n", - " \n", - "product_features = [\n", - " feature_product_quantity,\n", - " feature_product_price\n", - "]\n", - "\n", - "product_anchor = FeatureAnchor(name=\"product_anchored_features\",\n", - " source=product_batch_source,\n", - " features=product_features)" + "product_batch_source = HdfsSource(\n", + " name=\"productProfileData\",\n", + " path=product_detail_source_path,\n", + ")\n", + "\n", + "product_id = TypedKey(\n", + " key_column=\"product_id\",\n", + " key_column_type=ValueType.INT32,\n", + " description=\"product id\",\n", + " full_name=\"product_recommendation.product_id\",\n", + ")\n", + "\n", + "feature_product_quantity = Feature(\n", + " name=\"feature_product_quantity\",\n", + " key=product_id,\n", + " feature_type=FLOAT,\n", + " transform=\"quantity\",\n", + ")\n", + "feature_product_price = Feature(\n", + " name=\"feature_product_price\", key=product_id, feature_type=FLOAT, transform=\"price\"\n", + ")\n", + "\n", + "product_features = [feature_product_quantity, feature_product_price]\n", + "\n", + "product_feature_anchor = FeatureAnchor(\n", + " name=\"product_anchored_features\",\n", + " source=product_batch_source,\n", + " features=product_features,\n", + ")" ] }, { @@ -745,22 +763,27 @@ }, "outputs": [], "source": [ - "purchase_history_data = HdfsSource(name=\"purchase_history_data\",\n", - " path=\"wasbs://public@azurefeathrstorage.blob.core.windows.net/sample_data/product_recommendation_sample/user_purchase_history_mock_data.csv\",\n", - " event_timestamp_column=\"purchase_date\",\n", - " timestamp_format=\"yyyy-MM-dd\")\n", - " \n", - "agg_features = [Feature(name=\"feature_user_totla_purchase_in_90days\",\n", - " key=user_id,\n", - " feature_type=FLOAT,\n", - " transform=WindowAggTransformation(agg_expr=\"cast_float(purchase_amount)\",\n", - " agg_func=\"AVG\",\n", - " window=\"90d\"))\n", - " ]\n", - "\n", - "user_agg_feature_anchor = FeatureAnchor(name=\"aggregationFeatures\",\n", - " source=purchase_history_data,\n", - " features=agg_features)" + "purchase_history_data = HdfsSource(\n", + " name=\"purchase_history_data\",\n", + " path=user_purchase_history_source_path,\n", + " event_timestamp_column=\"purchase_date\",\n", + " timestamp_format=\"yyyy-MM-dd\",\n", + ")\n", + "\n", + "agg_features = [\n", + " Feature(\n", + " name=\"feature_user_avg_purchase_for_90days\",\n", + " key=user_id,\n", + " feature_type=FLOAT,\n", + " transform=WindowAggTransformation(\n", + " agg_expr=\"cast_float(purchase_amount)\", agg_func=\"AVG\", window=\"90d\"\n", + " ),\n", + " )\n", + "]\n", + "\n", + "user_agg_feature_anchor = FeatureAnchor(\n", + " name=\"aggregationFeatures\", source=purchase_history_data, features=agg_features\n", + ")" ] }, { @@ -791,12 +814,13 @@ }, "outputs": [], "source": [ - "feature_user_purchasing_power = DerivedFeature(name=\"feature_user_purchasing_power\",\n", - " key=user_id,\n", - " feature_type=FLOAT,\n", - " input_features=[\n", - " feature_user_gift_card_balance, feature_user_has_valid_credit_card],\n", - " transform=\"feature_user_gift_card_balance + if(boolean(feature_user_has_valid_credit_card), 100, 0)\")" + "feature_user_purchasing_power = DerivedFeature(\n", + " name=\"feature_user_purchasing_power\",\n", + " key=user_id,\n", + " feature_type=FLOAT,\n", + " input_features=[feature_user_gift_card_balance, feature_user_has_valid_credit_card],\n", + " transform=\"feature_user_gift_card_balance + if(boolean(feature_user_has_valid_credit_card), 100, 0)\",\n", + ")" ] }, { @@ -826,8 +850,10 @@ }, "outputs": [], "source": [ - "client.build_features(anchor_list=[user_agg_feature_anchor, user_feature_anchor, product_anchor], derived_feature_list=[\n", - " feature_user_purchasing_power])" + "client.build_features(\n", + " anchor_list=[user_agg_feature_anchor, user_feature_anchor, product_feature_anchor],\n", + " derived_feature_list=[feature_user_purchasing_power],\n", + ")" ] }, { @@ -864,38 +890,33 @@ }, "outputs": [], "source": [ - "if client.spark_runtime == 'databricks':\n", - " output_path = 'dbfs:/feathrazure_test.avro'\n", - "else:\n", - "# output_path = 'abfss://feathrazuretest3fs@feathrazuretest3storage.dfs.core.windows.net/test123_temp/product_rec_new'\n", - " output_path = feathr_output_path\n", - "\n", - "\n", "user_feature_query = FeatureQuery(\n", - " feature_list=[\"feature_user_age\", \n", - " \"feature_user_tax_rate\", \n", - " \"feature_user_gift_card_balance\", \n", - " \"feature_user_has_valid_credit_card\", \n", - " \"feature_user_totla_purchase_in_90days\",\n", - " \"feature_user_purchasing_power\"\n", - " ], \n", - " key=user_id)\n", + " feature_list=[\n", + " \"feature_user_age\",\n", + " \"feature_user_tax_rate\",\n", + " \"feature_user_gift_card_balance\",\n", + " \"feature_user_has_valid_credit_card\",\n", + " \"feature_user_avg_purchase_for_90days\",\n", + " \"feature_user_purchasing_power\",\n", + " ],\n", + " key=user_id,\n", + ")\n", "\n", "product_feature_query = FeatureQuery(\n", - " feature_list=[\n", - " \"feature_product_quantity\",\n", - " \"feature_product_price\"\n", - " ], \n", - " key=product_id)\n", + " feature_list=[\"feature_product_quantity\", \"feature_product_price\"], key=product_id\n", + ")\n", "\n", "settings = ObservationSettings(\n", - " observation_path=\"wasbs://public@azurefeathrstorage.blob.core.windows.net/sample_data/product_recommendation_sample/user_observation_mock_data.csv\",\n", + " observation_path=user_observation_source_path,\n", " event_timestamp_column=\"event_timestamp\",\n", - " timestamp_format=\"yyyy-MM-dd\")\n", - "client.get_offline_features(observation_settings=settings,\n", - " feature_query=[user_feature_query, product_feature_query],\n", - " output_path=output_path)\n", - "client.wait_job_to_finish(timeout_sec=1000)" + " timestamp_format=\"yyyy-MM-dd\",\n", + ")\n", + "client.get_offline_features(\n", + " observation_settings=settings,\n", + " feature_query=[user_feature_query, product_feature_query],\n", + " output_path=user_profile_source_path.rpartition(\"/\")[0] + f\"/product_recommendation_features.avro\",\n", + ")\n", + "client.wait_job_to_finish(timeout_sec=5000)" ] }, { @@ -927,22 +948,8 @@ }, "outputs": [], "source": [ - "def get_result_df(client: FeathrClient) -> pd.DataFrame:\n", - " \"\"\"Download the job result dataset from cloud as a Pandas dataframe.\"\"\"\n", - " res_url = client.get_job_result_uri(block=True, timeout_sec=600)\n", - " tmp_dir = tempfile.TemporaryDirectory()\n", - " client.feathr_spark_launcher.download_result(result_path=res_url, local_folder=tmp_dir.name)\n", - " dataframe_list = []\n", - " # assuming the result are in avro format\n", - " for file in glob.glob(os.path.join(tmp_dir.name, '*.avro')):\n", - " dataframe_list.append(pdx.read_avro(file))\n", - " vertical_concat_df = pd.concat(dataframe_list, axis=0)\n", - " tmp_dir.cleanup()\n", - " return vertical_concat_df\n", - "\n", - "df_res = get_result_df(client)\n", - "\n", - "df_res" + "res_df = get_result_df(client)\n", + "res_df.head()" ] }, { @@ -974,40 +981,29 @@ "outputs": [], "source": [ "from sklearn.ensemble import GradientBoostingRegressor\n", - "final_df = df_res\n", - "\n", - "final_df.drop([\"event_timestamp\"], axis=1, inplace=True, errors='ignore')\n", - "final_df.fillna(0, inplace=True)\n", - "final_df['product_rating'] = final_df['product_rating'].astype(\"float64\")\n", - "\n", - "train_x, test_x, train_y, test_y = train_test_split(final_df.drop([\"product_rating\"], axis=1),\n", - " final_df[\"product_rating\"],\n", - " test_size=0.2,\n", - " random_state=42)\n", - "model = GradientBoostingRegressor()\n", - "model.fit(train_x, train_y)\n", - "\n", - "y_predict = model.predict(test_x)\n", + "from sklearn.metrics import mean_squared_error\n", + "from sklearn.model_selection import train_test_split\n", "\n", - "y_actual = test_y.values.flatten().tolist()\n", - "rmse = sqrt(mean_squared_error(y_actual, y_predict))\n", "\n", - "sum_actuals = sum_errors = 0\n", + "final_df = (\n", + " res_df\n", + " .drop([\"event_timestamp\"], axis=1, errors=\"ignore\")\n", + " .fillna(0)\n", + ")\n", "\n", - "for actual_val, predict_val in zip(y_actual, y_predict):\n", - " abs_error = actual_val - predict_val\n", - " if abs_error < 0:\n", - " abs_error = abs_error * -1\n", + "X_train, X_test, y_train, y_test = train_test_split(\n", + " final_df.drop([\"product_rating\"], axis=1),\n", + " final_df[\"product_rating\"].astype(\"float64\"),\n", + " test_size=0.2,\n", + " random_state=42,\n", + ")\n", + "model = GradientBoostingRegressor()\n", + "model.fit(X_train, y_train)\n", "\n", - " sum_errors = sum_errors + abs_error\n", - " sum_actuals = sum_actuals + actual_val\n", + "y_pred = model.predict(X_test)\n", + "rmse = sqrt(mean_squared_error(y_test.values.flatten(), y_pred))\n", "\n", - "mean_abs_percent_error = sum_errors / sum_actuals\n", - "print(\"Model MAPE:\")\n", - "print(mean_abs_percent_error)\n", - "print()\n", - "print(\"Model Accuracy:\")\n", - "print(1 - mean_abs_percent_error)\n" + "print(f\"Root mean squared error: {rmse}\")" ] }, { @@ -1029,6 +1025,15 @@ "We can push the generated features to the online store like below:" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pd.read_csv(user_profile_file_path).head()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1043,19 +1048,25 @@ "outputs": [], "source": [ "# Materialize user features\n", - "# (You can only materialize features of same entity key into one table so we can only materialize user features first.)\n", - "backfill_time = BackfillTime(start=datetime(\n", - " 2020, 5, 20), end=datetime(2020, 5, 20), step=timedelta(days=1))\n", + "# Note, you can only materialize features of same entity key into one table.\n", "redisSink = RedisSink(table_name=\"user_features\")\n", - "settings = MaterializationSettings(\"user_feature_setting\",\n", - " backfill_time=backfill_time,\n", - " sinks=[redisSink],\n", - " feature_names=[\"feature_user_age\", \"feature_user_gift_card_balance\"])\n", - "\n", - "client.materialize_features(settings, allow_materialize_non_agg_feature =True)\n", - "client.wait_job_to_finish(timeout_sec=1000)" + "settings = MaterializationSettings(\n", + " name=\"user_feature_setting\",\n", + " sinks=[redisSink],\n", + " feature_names=[\"feature_user_age\", \"feature_user_gift_card_balance\"],\n", + ")\n", + "\n", + "client.materialize_features(settings=settings, allow_materialize_non_agg_feature=True)\n", + "client.wait_job_to_finish(timeout_sec=5000)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "metadata": { @@ -1100,8 +1111,9 @@ }, "outputs": [], "source": [ - "client.get_online_features('user_features', '2', [\n", - " 'feature_user_age', 'feature_user_gift_card_balance'])" + "client.get_online_features(\n", + " \"user_features\", \"2\", [\"feature_user_age\", \"feature_user_gift_card_balance\"]\n", + ")" ] }, { @@ -1117,8 +1129,9 @@ }, "outputs": [], "source": [ - "client.multi_get_online_features('user_features', ['1', '2'], [\n", - " 'feature_user_age', 'feature_user_gift_card_balance'])\n" + "client.multi_get_online_features(\n", + " \"user_features\", [\"1\", \"2\"], [\"feature_user_age\", \"feature_user_gift_card_balance\"]\n", + ")" ] }, { @@ -1151,15 +1164,21 @@ "outputs": [], "source": [ "# Materialize product features\n", - "backfill_time = BackfillTime(start=datetime(\n", - " 2020, 5, 20), end=datetime(2020, 5, 20), step=timedelta(days=1))\n", - "redisSink = RedisSink(table_name=\"product_features\")\n", - "settings = MaterializationSettings(\"product_feature_setting\",\n", - " backfill_time=backfill_time,\n", - " sinks=[redisSink],\n", - " feature_names=[\"feature_product_price\"])\n", + "backfill_time = BackfillTime(\n", + " start=datetime(2020, 5, 20),\n", + " end=datetime(2020, 5, 20),\n", + " step=timedelta(days=1),\n", + ")\n", "\n", - "client.materialize_features(settings, allow_materialize_non_agg_feature =True)\n", + "redisSink = RedisSink(table_name=\"product_features\")\n", + "settings = MaterializationSettings(\n", + " \"product_feature_setting\",\n", + " backfill_time=backfill_time,\n", + " sinks=[redisSink],\n", + " feature_names=[\"feature_product_price\"],\n", + ")\n", + "\n", + "client.materialize_features(settings, allow_materialize_non_agg_feature=True)\n", "client.wait_job_to_finish(timeout_sec=1000)" ] }, @@ -1176,14 +1195,11 @@ }, "outputs": [], "source": [ - "client.get_online_features('product_feature_setting', '2', [\n", - " 'feature_product_price'])\n", - "\n", - "client.get_online_features('product_features', '2', [\n", - " 'feature_product_price'])" + "client.get_online_features(\"product_features\", \"2\", [\"feature_product_price\"])" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -1194,7 +1210,7 @@ } }, "source": [ - "### Registering and Fetching features\n", + "## Registering and Fetching features\n", "\n", "We can also register the features with an Apache Atlas compatible service, such as Azure Purview, and share the registered features across teams:" ] @@ -1212,8 +1228,13 @@ }, "outputs": [], "source": [ - "client.register_features()\n", - "client.list_registered_features(project_name=\"feathr_getting_started\")" + "if REGISTER_FEATURES:\n", + " try:\n", + " client.register_features()\n", + " except KeyError:\n", + " # TODO temporarily go around the \"Already exists\" error\n", + " pass\n", + " print(client.list_registered_features(project_name=PROJECT_NAME))" ] } ], @@ -1229,7 +1250,7 @@ "widgets": {} }, "kernelspec": { - "display_name": "Python 3.9.13 64-bit ('3.9.13')", + "display_name": "feathr", "language": "python", "name": "python3" }, @@ -1243,11 +1264,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.10.8" }, "vscode": { "interpreter": { - "hash": "c5d1b88564ea095927319e95d120a01ba9530a1c584720276480e541fd6461c7" + "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" } } }, diff --git a/feathr_project/feathr/datasets/constants.py b/feathr_project/feathr/datasets/constants.py index 13c33713c..91230d8da 100644 --- a/feathr_project/feathr/datasets/constants.py +++ b/feathr_project/feathr/datasets/constants.py @@ -1,7 +1,11 @@ +# NYC Taxi fare prediction sample dataset. +# Ref: https://www1.nyc.gov/site/tlc/about/tlc-trip-record-data.page NYC_TAXI_SMALL_URL = ( "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/green_tripdata_2020-04_with_index.csv" ) +# Fraud detection sample datasets. +# Ref: https://github.com/microsoft/r-server-fraud-detection FRAUD_DETECTION_ACCOUNT_INFO_URL = ( "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/frauddetection/Account_Info.csv" ) @@ -13,3 +17,27 @@ FRAUD_DETECTION_UNTAGGED_TRANSACTIONS_URL = ( "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/frauddetection/Untagged_Transactions.csv" ) + +# Product recommendation sample datasets. +# Ref: +PRODUCT_RECOMMENDATION_USER_OBSERVATION_URL = ( + "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/product_recommendation_sample/user_observation_mock_data.csv" +) + +PRODUCT_RECOMMENDATION_USER_PROFILE_URL = ( + "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/product_recommendation_sample/user_profile_mock_data.csv" +) + +PRODUCT_RECOMMENDATION_USER_PURCHASE_HISTORY_URL = ( + "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/product_recommendation_sample/user_purchase_history_mock_data.csv" +) + +PRODUCT_RECOMMENDATION_PRODUCT_DETAIL_URL = ( + "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/product_recommendation_sample/product_detail_mock_data.csv" +) + +# Hotel review sample datasets. +# Ref: https://www.kaggle.com/datasets/datafiniti/hotel-reviews +HOTEL_REVIEWS_URL = ( + "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/hotel_reviews_100_with_id.csv" +) From 62e42c07e61260bcd532e022c25c56cfae2c522c Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Sat, 17 Dec 2022 00:07:43 +0000 Subject: [PATCH 09/22] Update synapse example. Add azure-cli dependency to notebook dependencies Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- .../product_recommendation_demo.ipynb | 544 +++++++++--------- ...product_recommendation_demo_advanced.ipynb | 28 +- feathr_project/setup.py | 1 + 3 files changed, 292 insertions(+), 281 deletions(-) diff --git a/docs/samples/azure_synapse/product_recommendation_demo.ipynb b/docs/samples/azure_synapse/product_recommendation_demo.ipynb index 4a6a54cbf..43912d99a 100644 --- a/docs/samples/azure_synapse/product_recommendation_demo.ipynb +++ b/docs/samples/azure_synapse/product_recommendation_demo.ipynb @@ -34,44 +34,40 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "## 2. Prerequisite: Install Feathr and it's dependencies and Login to Azure" + "## 2. Prerequisite: Set the required permissions\n", + "\n", + "Before you proceed further, you would need additional permissions: permission to access the keyvault, permission to access the Storage Blob as a Contributor and permission to submit jobs to Synapse cluster. Run the following lines of command in the [Cloud Shell](https://shell.azure.com) before running the cells below. Please replace the resource_prefix with the prefix you used in ARM template deployment.\n", + "\n", + "```\n", + " resource_prefix=\"YOUR_RESOURCE_PREFIX\"\n", + " synapse_workspace_name=\"${resource_prefix}syws\"\n", + " keyvault_name=\"${resource_prefix}kv\"\n", + " objectId=$(az ad signed-in-user show --query id -o tsv)\n", + " az keyvault update --name $keyvault_name --enable-rbac-authorization false\n", + " az keyvault set-policy -n $keyvault_name --secret-permissions get list --object-id $objectId\n", + " az role assignment create --assignee $userId --role \"Storage Blob Data Contributor\"\n", + " az synapse role assignment create --workspace-name $synapse_workspace_name --role \"Synapse Contributor\" --assignee $userId\n", + "```\n" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Install Feathr and dependencies to run this notebook. Normally you could run all the pip installs in one line, but when running this notebook in synapse, you may get some errors or blocks installing above packages in one cell. Hence installing them in different cells." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install -U feathr" + "## 3. Prerequisite: Install Feathr and it's dependencies and Login to Azure" ] }, { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install -U azure-cli" - ] - }, - { - "cell_type": "code", - "execution_count": null, + "attachments": {}, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "%pip install -U pandavro" + "Install Feathr and dependencies to run this notebook." ] }, { @@ -80,14 +76,15 @@ "metadata": {}, "outputs": [], "source": [ - "%pip install -U scikit-learn" + "!pip install \"git+https://github.com/feathr-ai/feathr.git#subdirectory=feathr_project&egg=feathr[notebook]\"" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "Login to Azure with a device code (You will see instructions in the output once you execute the cell):" + "If you meet errors like 'cannot import FeatherClient from feathr', it may be caused by incompatible version of 'aiohttp'. Please try to install/upgrade it by running the following command:" ] }, { @@ -96,7 +93,7 @@ "metadata": {}, "outputs": [], "source": [ - "! az login --use-device-code" + "!pip install aiohttp==3.8.3" ] }, { @@ -118,49 +115,63 @@ "from datetime import datetime, timedelta\n", "from math import sqrt\n", "\n", - "import pandas as pd\n", - "import pandavro as pdx\n", - "from feathr import FeathrClient\n", - "from feathr import BOOLEAN, FLOAT, INT32, ValueType\n", - "from feathr import Feature, DerivedFeature, FeatureAnchor\n", - "from feathr import BackfillTime, MaterializationSettings\n", - "from feathr import FeatureQuery, ObservationSettings\n", - "from feathr import RedisSink\n", - "from feathr import INPUT_CONTEXT, HdfsSource\n", - "from feathr import WindowAggTransformation\n", - "from feathr import TypedKey\n", - "from sklearn.metrics import mean_squared_error\n", - "from sklearn.model_selection import train_test_split\n", "from azure.identity import AzureCliCredential\n", - "from azure.keyvault.secrets import SecretClient" + "from azure.keyvault.secrets import SecretClient\n", + " \n", + "import pandas as pd\n", + "from pyspark.sql import DataFrame\n", + "\n", + "import feathr\n", + "from feathr import (\n", + " FeathrClient,\n", + " BOOLEAN, FLOAT, INT32, ValueType,\n", + " Feature, DerivedFeature, FeatureAnchor,\n", + " BackfillTime, MaterializationSettings,\n", + " FeatureQuery, ObservationSettings,\n", + " RedisSink,\n", + " INPUT_CONTEXT, HdfsSource,\n", + " WindowAggTransformation,\n", + " TypedKey,\n", + ")\n", + "from feathr.datasets.constants import (\n", + " PRODUCT_RECOMMENDATION_USER_OBSERVATION_URL,\n", + " PRODUCT_RECOMMENDATION_USER_PROFILE_URL,\n", + " PRODUCT_RECOMMENDATION_USER_PURCHASE_HISTORY_URL,\n", + " PRODUCT_RECOMMENDATION_PRODUCT_DETAIL_URL,\n", + ")\n", + "from feathr.datasets.utils import maybe_download\n", + "from feathr.utils.config import generate_config\n", + "from feathr.utils.job_utils import get_result_df\n", + "from feathr.utils.platform import is_databricks\n", + "\n", + "\n", + "print(f\"Feathr version: {feathr.__version__}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Login to Azure with a device code (You will see instructions in the output once you execute the cell):" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "If you meet errors like 'cannot import FeatherClient from feathr', it may be caused by incompatible version of 'aiohttp'. Please try to install/upgrade it by running: '%pip install aiohttp==3.8.3'" + "!az login --use-device-code" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "\n", - "## 3. Prerequisite: Set the required permissions\n", - "\n", - "Before you proceed further, you would need additional permissions: permission to access the keyvault, permission to access the Storage Blob as a Contributor and permission to submit jobs to Synapse cluster. Run the following lines of command in the [Cloud Shell](https://shell.azure.com) before running the cells below. Please replace the resource_prefix with the prefix you used in ARM template deployment.\n", - "\n", - "```\n", - " resource_prefix=\"YOUR_RESOURCE_PREFIX\"\n", - " synapse_workspace_name=\"${resource_prefix}syws\"\n", - " keyvault_name=\"${resource_prefix}kv\"\n", - " objectId=$(az ad signed-in-user show --query id -o tsv)\n", - " az keyvault update --name $keyvault_name --enable-rbac-authorization false\n", - " az keyvault set-policy -n $keyvault_name --secret-permissions get list --object-id $objectId\n", - " az role assignment create --assignee $userId --role \"Storage Blob Data Contributor\"\n", - " az synapse role assignment create --workspace-name $synapse_workspace_name --role \"Synapse Contributor\" --assignee $userId\n", - "```\n" + "credential = AzureCliCredential(additionally_allowed_tenants=['*'])" ] }, { @@ -171,12 +182,13 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "# 4. Prerequisite: Feathr Configuration\n", + "# Feathr Configuration\n", "\n", - "### Setting the environment variables\n", + "## Setting the environment variables\n", "Set the environment variables that will be used by Feathr as configuration. Feathr supports configuration via enviroment variables and yaml, you can read more about it [here](https://feathr-ai.github.io/feathr/how-to-guides/feathr-configuration-and-env.html).\n", "\n", "**Fill in the `resource_prefix` that you used while provisioning the resources in Step 1 using ARM.**" @@ -188,8 +200,23 @@ "metadata": {}, "outputs": [], "source": [ - "RESOURCE_PREFIX = \"YOUR_RESOURCE_PREFIX\" # from ARM deployment in Step 1\n", - "FEATHR_PROJECT_NAME=\"YOUR_PROJECT_NAME\" # provide a unique name" + "# TODO fill the following values\n", + "RESOURCE_PREFIX = None # The prefix value used at the ARM deployment step\n", + "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", + "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", + "\n", + "PROJECT_NAME = \"product_recommendation_synapse_demo\"\n", + "SPARK_CLUSTER = \"azure_synapse\"\n", + "\n", + "# TODO if you deployed resources manually using different names, you'll need to change the following values accordingly: \n", + "ADLS_ACCOUNT=f\"{RESOURCE_PREFIX}dls\"\n", + "ADLS_FS_NAME=f\"{RESOURCE_PREFIX}fs\"\n", + "AZURE_SYNAPSE_URL = f\"https://{RESOURCE_PREFIX}syws.dev.azuresynapse.net\" # Set Azure Synapse workspace url to use Azure Synapse\n", + "KEY_VAULT_URI = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", + "REDIS_HOST = f\"{RESOURCE_PREFIX}redis.redis.cache.windows.net\"\n", + "REGISTRY_ENDPOINT = f\"https://{RESOURCE_PREFIX}webapp.azurewebsites.net/api/v1\"\n", + "\n", + "WORKING_DIR = f\"abfss://{ADLS_FS_NAME}@{ADLS_ACCOUNT}.dfs.core.windows.net/{PROJECT_NAME}\"\n" ] }, { @@ -198,30 +225,20 @@ "metadata": {}, "outputs": [], "source": [ - "\n", - "# Get name for deployed resources using the resource prefix\n", - "KEY_VAULT_NAME=f\"{RESOURCE_PREFIX}kv\"\n", - "SYNAPSE_WORKSPACE_NAME=f\"{RESOURCE_PREFIX}syws\"\n", - "ADLS_ACCOUNT=f\"{RESOURCE_PREFIX}dls\"\n", - "ADLS_FS_NAME=f\"{RESOURCE_PREFIX}fs\"\n", - "KEY_VAULT_URI = f\"https://{KEY_VAULT_NAME}.vault.azure.net\"\n", - "FEATHR_API_APP = f\"{RESOURCE_PREFIX}webapp\"\n", - "\n", - "\n", - "# Getting the credential object for Key Vault client\n", - "credential = AzureCliCredential()\n", - "client = SecretClient(vault_url=KEY_VAULT_URI, credential=credential)\n", - "\n", - "# Getting Redis store's connection string.\n", - "retrieved_secret = client.get_secret(\"FEATHR-ONLINE-STORE-CONN\").value\n", - "\n", - "# Parse Redis connection string\n", - "REDIS_PORT=retrieved_secret.split(',')[0].split(\":\")[1]\n", - "REDIS_HOST=retrieved_secret.split(',')[0].split(\":\")[0]\n", - "REDIS_PASSWORD=retrieved_secret.split(',')[1].split(\"password=\",1)[1]\n", - "REDIS_SSL=retrieved_secret.split(',')[2].split(\"ssl=\",1)[1]\n", - "# Set password as environment variable.\n", - "os.environ['REDIS_PASSWORD']=REDIS_PASSWORD" + "if \"ADLS_KEY\" not in os.environ and ADLS_KEY:\n", + " os.environ[\"ADLS_KEY\"] = ADLS_KEY" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if \"REDIS_PASSWORD\" not in os.environ:\n", + " secret_client = SecretClient(vault_url=KEY_VAULT_URI, credential=credential)\n", + " retrieved_secret = secret_client.get_secret('FEATHR-ONLINE-STORE-CONN').value\n", + " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]" ] }, { @@ -239,39 +256,19 @@ "metadata": {}, "outputs": [], "source": [ - "import tempfile\n", - "yaml_config = f\"\"\"\n", - "api_version: 1\n", - "project_config:\n", - " project_name: '{FEATHR_PROJECT_NAME}'\n", - "offline_store:\n", - "# Please set 'enabled' flags as true (false by default) if any of items under the same paths are expected to be visited\n", - " adls:\n", - " adls_enabled: true\n", - " wasb:\n", - " wasb_enabled: true\n", - "spark_config:\n", - " spark_cluster: 'azure_synapse'\n", - " spark_result_output_parts: '1'\n", - " azure_synapse:\n", - " dev_url: 'https://{SYNAPSE_WORKSPACE_NAME}.dev.azuresynapse.net'\n", - " pool_name: 'spark31'\n", - " workspace_dir: 'abfss://{ADLS_FS_NAME}@{ADLS_ACCOUNT}.dfs.core.windows.net/feathr_project'\n", - " executor_size: 'Small'\n", - " executor_num: 1\n", - "online_store:\n", - " redis:\n", - " host: '{REDIS_HOST}'\n", - " port: {REDIS_PORT}\n", - " ssl_enabled: {REDIS_SSL}\n", - "feature_registry:\n", - " api_endpoint: 'https://{FEATHR_API_APP}.azurewebsites.net/api/v1'\n", - "\"\"\"\n", - "\n", - "tmp = tempfile.NamedTemporaryFile(mode='w', delete=False)\n", - "with open(tmp.name, \"w\") as text_file:\n", - " text_file.write(yaml_config)\n", - "feathr_output_path = f'abfss://{ADLS_FS_NAME}@{ADLS_ACCOUNT}.dfs.core.windows.net/feathr_output'" + "config_path = generate_config(\n", + " resource_prefix=RESOURCE_PREFIX,\n", + " project_name=PROJECT_NAME,\n", + " online_store__redis__host=REDIS_HOST,\n", + " feature_registry__api_endpoint=REGISTRY_ENDPOINT,\n", + " spark_config__spark_cluster=SPARK_CLUSTER,\n", + " spark_config__azure_synapse__dev_url=AZURE_SYNAPSE_URL,\n", + " spark_config__azure_synapse__pool_name=AZURE_SYNAPSE_SPARK_POOL,\n", + " spark_config__azure_synapse__workspace_dir=WORKING_DIR,\n", + ")\n", + "\n", + "with open(config_path, 'r') as f: \n", + " print(f.read())" ] }, { @@ -294,7 +291,7 @@ "metadata": {}, "outputs": [], "source": [ - "feathr_client = FeathrClient(config_path=tmp.name, credential=credential)" + "client = FeathrClient(config_path=config_path, credential=credential)" ] }, { @@ -305,6 +302,24 @@ "We have 3 raw datasets to work with: one observation dataset(a.k.a. label dataset) and two raw datasets to generate features." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Upload datasets into ADLS\n", + "user_observation_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(\n", + " PRODUCT_RECOMMENDATION_USER_OBSERVATION_URL\n", + ")\n", + "user_profile_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(\n", + " PRODUCT_RECOMMENDATION_USER_PROFILE_URL\n", + ")\n", + "user_purchase_history_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(\n", + " PRODUCT_RECOMMENDATION_USER_PURCHASE_HISTORY_URL\n", + ")" + ] + }, { "cell_type": "code", "execution_count": null, @@ -314,9 +329,7 @@ "# Observation dataset(a.k.a. label dataset)\n", "# Observation dataset usually comes with a event_timestamp to denote when the observation happened.\n", "# The label here is product_rating. Our model objective is to predict a user's rating for this product.\n", - "import pandas as pd\n", - "# Public URL hosting mock data\n", - "pd.read_csv(\"https://azurefeathrstorage.blob.core.windows.net/public/sample_data/product_recommendation_sample/user_observation_mock_data.csv\")" + "pd.read_csv(user_observation_source_path).head()" ] }, { @@ -327,8 +340,7 @@ "source": [ "# User profile dataset\n", "# Used to generate user features\n", - "import pandas as pd\n", - "pd.read_csv(\"https://azurefeathrstorage.blob.core.windows.net/public/sample_data/product_recommendation_sample/user_profile_mock_data.csv\")" + "pd.read_csv(user_profile_source_path).head()" ] }, { @@ -339,8 +351,7 @@ "source": [ "# User purchase history dataset.\n", "# Used to generate user features. This is activity type data, so we need to use aggregation to genearte features.\n", - "import pandas as pd\n", - "pd.read_csv(\"https://azurefeathrstorage.blob.core.windows.net/public/sample_data/product_recommendation_sample/user_purchase_history_mock_data.csv\")" + "pd.read_csv(user_purchase_history_source_path).head()" ] }, { @@ -396,16 +407,18 @@ "metadata": {}, "outputs": [], "source": [ - "from pyspark.sql import SparkSession, DataFrame\n", "def feathr_udf_preprocessing(df: DataFrame) -> DataFrame:\n", " from pyspark.sql.functions import col\n", - " df = df.withColumn(\"tax_rate_decimal\", col(\"tax_rate\")/100)\n", - " df.show(10)\n", + "\n", + " df = df.withColumn(\"tax_rate_decimal\", col(\"tax_rate\") / 100)\n", " return df\n", "\n", - "batch_source = HdfsSource(name=\"userProfileData\",\n", - " path=\"wasbs://public@azurefeathrstorage.blob.core.windows.net/sample_data/product_recommendation_sample/user_profile_mock_data.csv\",\n", - " preprocessing=feathr_udf_preprocessing)" + "\n", + "batch_source = HdfsSource(\n", + " name=\"userProfileData\",\n", + " path=user_profile_source_path,\n", + " preprocessing=feathr_udf_preprocessing,\n", + ")" ] }, { @@ -427,38 +440,49 @@ "metadata": {}, "outputs": [], "source": [ - "user_id = TypedKey(key_column=\"user_id\",\n", - " key_column_type=ValueType.INT32,\n", - " description=\"user id\",\n", - " full_name=\"product_recommendation.user_id\")\n", - "\n", - "feature_user_age = Feature(name=\"feature_user_age\",\n", - " key=user_id,\n", - " feature_type=INT32, \n", - " transform=\"age\")\n", - "feature_user_tax_rate = Feature(name=\"feature_user_tax_rate\",\n", - " key=user_id,\n", - " feature_type=FLOAT,\n", - " transform=\"tax_rate_decimal\")\n", - "feature_user_gift_card_balance = Feature(name=\"feature_user_gift_card_balance\",\n", - " key=user_id,\n", - " feature_type=FLOAT,\n", - " transform=\"gift_card_balance\")\n", - "feature_user_has_valid_credit_card = Feature(name=\"feature_user_has_valid_credit_card\",\n", - " key=user_id,\n", - " feature_type=BOOLEAN,\n", - " transform=\"number_of_credit_cards > 0\")\n", - " \n", + "# Let's define some features for users so our recommendation can be customized for users.\n", + "user_id = TypedKey(\n", + " key_column=\"user_id\",\n", + " key_column_type=ValueType.INT32,\n", + " description=\"user id\",\n", + " full_name=\"product_recommendation.user_id\",\n", + ")\n", + "\n", + "feature_user_age = Feature(\n", + " name=\"feature_user_age\",\n", + " key=user_id,\n", + " feature_type=INT32,\n", + " transform=\"age\",\n", + ")\n", + "feature_user_tax_rate = Feature(\n", + " name=\"feature_user_tax_rate\",\n", + " key=user_id,\n", + " feature_type=FLOAT,\n", + " transform=\"tax_rate_decimal\",\n", + ")\n", + "feature_user_gift_card_balance = Feature(\n", + " name=\"feature_user_gift_card_balance\",\n", + " key=user_id,\n", + " feature_type=FLOAT,\n", + " transform=\"gift_card_balance\",\n", + ")\n", + "feature_user_has_valid_credit_card = Feature(\n", + " name=\"feature_user_has_valid_credit_card\",\n", + " key=user_id,\n", + " feature_type=BOOLEAN,\n", + " transform=\"number_of_credit_cards > 0\",\n", + ")\n", + "\n", "features = [\n", " feature_user_age,\n", " feature_user_tax_rate,\n", " feature_user_gift_card_balance,\n", - " feature_user_has_valid_credit_card\n", + " feature_user_has_valid_credit_card,\n", "]\n", "\n", - "request_anchor = FeatureAnchor(name=\"anchored_features\",\n", - " source=batch_source,\n", - " features=features)" + "user_feature_anchor = FeatureAnchor(\n", + " name=\"anchored_features\", source=batch_source, features=features\n", + ")" ] }, { @@ -493,22 +517,27 @@ "metadata": {}, "outputs": [], "source": [ - "purchase_history_data = HdfsSource(name=\"purchase_history_data\",\n", - " path=\"wasbs://public@azurefeathrstorage.blob.core.windows.net/sample_data/product_recommendation_sample/user_purchase_history_mock_data.csv\",\n", - " event_timestamp_column=\"purchase_date\",\n", - " timestamp_format=\"yyyy-MM-dd\")\n", - " \n", - "agg_features = [Feature(name=\"feature_user_total_purchase_in_90days\",\n", - " key=user_id,\n", - " feature_type=FLOAT,\n", - " transform=WindowAggTransformation(agg_expr=\"cast_float(purchase_amount)\",\n", - " agg_func=\"AVG\",\n", - " window=\"90d\"))\n", - " ]\n", + "purchase_history_data = HdfsSource(\n", + " name=\"purchase_history_data\",\n", + " path=user_purchase_history_source_path,\n", + " event_timestamp_column=\"purchase_date\",\n", + " timestamp_format=\"yyyy-MM-dd\",\n", + ")\n", + "\n", + "agg_features = [\n", + " Feature(\n", + " name=\"feature_user_avg_purchase_for_90days\",\n", + " key=user_id,\n", + " feature_type=FLOAT,\n", + " transform=WindowAggTransformation(\n", + " agg_expr=\"cast_float(purchase_amount)\", agg_func=\"AVG\", window=\"90d\"\n", + " ),\n", + " )\n", + "]\n", "\n", - "agg_anchor = FeatureAnchor(name=\"aggregationFeatures\",\n", - " source=purchase_history_data,\n", - " features=agg_features)" + "user_agg_feature_anchor = FeatureAnchor(\n", + " name=\"aggregationFeatures\", source=purchase_history_data, features=agg_features\n", + ")" ] }, { @@ -527,11 +556,13 @@ "metadata": {}, "outputs": [], "source": [ - "feature_user_purchasing_power = DerivedFeature(name=\"feature_user_purchasing_power\",\n", - " key=user_id,\n", - " feature_type=FLOAT,\n", - " input_features=[feature_user_gift_card_balance, feature_user_has_valid_credit_card],\n", - " transform=\"feature_user_gift_card_balance + if(boolean(feature_user_has_valid_credit_card), 100, 0)\")" + "feature_user_purchasing_power = DerivedFeature(\n", + " name=\"feature_user_purchasing_power\",\n", + " key=user_id,\n", + " feature_type=FLOAT,\n", + " input_features=[feature_user_gift_card_balance, feature_user_has_valid_credit_card],\n", + " transform=\"feature_user_gift_card_balance + if(boolean(feature_user_has_valid_credit_card), 100, 0)\",\n", + ")" ] }, { @@ -548,8 +579,10 @@ "metadata": {}, "outputs": [], "source": [ - "feathr_client.build_features(anchor_list=[agg_anchor, request_anchor], \n", - " derived_feature_list=[feature_user_purchasing_power])" + "client.build_features(\n", + " anchor_list=[user_agg_feature_anchor, user_feature_anchor],\n", + " derived_feature_list=[feature_user_purchasing_power],\n", + ")" ] }, { @@ -583,23 +616,29 @@ "metadata": {}, "outputs": [], "source": [ - "output_path = feathr_output_path\n", - "# Features that we want to request\n", - "feature_query = FeatureQuery(feature_list=[\"feature_user_age\", \n", - " \"feature_user_tax_rate\", \n", - " \"feature_user_gift_card_balance\", \n", - " \"feature_user_has_valid_credit_card\", \n", - " \"feature_user_total_purchase_in_90days\",\n", - " \"feature_user_purchasing_power\"], \n", - " key=user_id)\n", + "user_feature_query = FeatureQuery(\n", + " feature_list=[\n", + " \"feature_user_age\",\n", + " \"feature_user_tax_rate\",\n", + " \"feature_user_gift_card_balance\",\n", + " \"feature_user_has_valid_credit_card\",\n", + " \"feature_user_avg_purchase_for_90days\",\n", + " \"feature_user_purchasing_power\",\n", + " ],\n", + " key=user_id,\n", + ")\n", + "\n", "settings = ObservationSettings(\n", - " observation_path=\"wasbs://public@azurefeathrstorage.blob.core.windows.net/sample_data/product_recommendation_sample/user_observation_mock_data.csv\",\n", + " observation_path=user_observation_source_path,\n", " event_timestamp_column=\"event_timestamp\",\n", - " timestamp_format=\"yyyy-MM-dd\")\n", - "feathr_client.get_offline_features(observation_settings=settings,\n", - " feature_query=feature_query,\n", - " output_path=output_path)\n", - "feathr_client.wait_job_to_finish(timeout_sec=500)" + " timestamp_format=\"yyyy-MM-dd\",\n", + ")\n", + "client.get_offline_features(\n", + " observation_settings=settings,\n", + " feature_query=[user_feature_query],\n", + " output_path=user_profile_source_path.rpartition(\"/\")[0] + f\"/product_recommendation_features.avro\",\n", + ")\n", + "client.wait_job_to_finish(timeout_sec=5000)" ] }, { @@ -617,22 +656,8 @@ "metadata": {}, "outputs": [], "source": [ - "def get_result_df(client: FeathrClient) -> pd.DataFrame:\n", - " \"\"\"Download the job result dataset from cloud as a Pandas dataframe.\"\"\"\n", - " res_url = client.get_job_result_uri(block=True, timeout_sec=600)\n", - " tmp_dir = tempfile.TemporaryDirectory()\n", - " client.feathr_spark_launcher.download_result(result_path=res_url, local_folder=tmp_dir.name)\n", - " dataframe_list = []\n", - " # assuming the result are in avro format\n", - " for file in glob.glob(os.path.join(tmp_dir.name, '*.avro')):\n", - " dataframe_list.append(pdx.read_avro(file))\n", - " vertical_concat_df = pd.concat(dataframe_list, axis=0)\n", - " tmp_dir.cleanup()\n", - " return vertical_concat_df\n", - "\n", - "df_res = get_result_df(feathr_client)\n", - "\n", - "df_res" + "res_df = get_result_df(client)\n", + "res_df.head()" ] }, { @@ -649,41 +674,30 @@ "metadata": {}, "outputs": [], "source": [ - "# drop non-feature columns\n", "from sklearn.ensemble import GradientBoostingRegressor\n", - "final_df = df_res\n", - "final_df.drop([\"event_timestamp\"], axis=1, inplace=True, errors='ignore')\n", - "final_df.fillna(0, inplace=True)\n", - "final_df['product_rating'] = final_df['product_rating'].astype(\"float64\")\n", - "\n", - "train_x, test_x, train_y, test_y = train_test_split(final_df.drop([\"product_rating\"], axis=1),\n", - " final_df[\"product_rating\"],\n", - " test_size=0.2,\n", - " random_state=42)\n", - "model = GradientBoostingRegressor()\n", - "model.fit(train_x, train_y)\n", - "\n", - "y_predict = model.predict(test_x)\n", + "from sklearn.metrics import mean_squared_error\n", + "from sklearn.model_selection import train_test_split\n", "\n", - "y_actual = test_y.values.flatten().tolist()\n", - "rmse = sqrt(mean_squared_error(y_actual, y_predict))\n", "\n", - "sum_actuals = sum_errors = 0\n", + "final_df = (\n", + " res_df\n", + " .drop([\"event_timestamp\"], axis=1, errors=\"ignore\")\n", + " .fillna(0)\n", + ")\n", "\n", - "for actual_val, predict_val in zip(y_actual, y_predict):\n", - " abs_error = actual_val - predict_val\n", - " if abs_error < 0:\n", - " abs_error = abs_error * -1\n", + "X_train, X_test, y_train, y_test = train_test_split(\n", + " final_df.drop([\"product_rating\"], axis=1),\n", + " final_df[\"product_rating\"].astype(\"float64\"),\n", + " test_size=0.2,\n", + " random_state=42,\n", + ")\n", + "model = GradientBoostingRegressor()\n", + "model.fit(X_train, y_train)\n", "\n", - " sum_errors = sum_errors + abs_error\n", - " sum_actuals = sum_actuals + actual_val\n", + "y_pred = model.predict(X_test)\n", + "rmse = sqrt(mean_squared_error(y_test.values.flatten(), y_pred))\n", "\n", - "mean_abs_percent_error = sum_errors / sum_actuals\n", - "print(\"Model MAPE:\")\n", - "print(mean_abs_percent_error)\n", - "print()\n", - "print(\"Model Accuracy:\")\n", - "print(1 - mean_abs_percent_error)\n" + "print(f\"Root mean squared error: {rmse}\")" ] }, { @@ -712,17 +726,17 @@ "metadata": {}, "outputs": [], "source": [ - "backfill_time = BackfillTime(start=datetime(2020, 5, 20), \n", - " end=datetime(2020, 5, 20), \n", - " step=timedelta(days=1))\n", - "redisSink = RedisSink(table_name=\"productRecommendationDemoFeature\")\n", - "settings = MaterializationSettings(name=\"productRecommendationFeatureSetting\",\n", - " backfill_time=backfill_time,\n", - " sinks=[redisSink],\n", - " feature_names=[\"feature_user_age\", \"feature_user_gift_card_balance\"])\n", + "# Materialize user features\n", + "# Note, you can only materialize features of same entity key into one table.\n", + "redisSink = RedisSink(table_name=\"user_features\")\n", + "settings = MaterializationSettings(\n", + " name=\"user_feature_setting\",\n", + " sinks=[redisSink],\n", + " feature_names=[\"feature_user_age\", \"feature_user_gift_card_balance\"],\n", + ")\n", "\n", - "feathr_client.materialize_features(settings, allow_materialize_non_agg_feature =True)\n", - "feathr_client.wait_job_to_finish(timeout_sec=500)" + "client.materialize_features(settings=settings, allow_materialize_non_agg_feature=True)\n", + "client.wait_job_to_finish(timeout_sec=5000)" ] }, { @@ -739,9 +753,9 @@ "metadata": {}, "outputs": [], "source": [ - "feathr_client.get_online_features('productRecommendationDemoFeature', \n", - " '2', \n", - " ['feature_user_age', 'feature_user_gift_card_balance'])" + "client.get_online_features(\n", + " \"user_features\", \"2\", [\"feature_user_age\", \"feature_user_gift_card_balance\"]\n", + ")" ] }, { @@ -750,9 +764,9 @@ "metadata": {}, "outputs": [], "source": [ - "feathr_client.multi_get_online_features('productRecommendationDemoFeature', \n", - " ['1', '2'], \n", - " ['feature_user_age', 'feature_user_gift_card_balance'])" + "client.multi_get_online_features(\n", + " \"user_features\", [\"1\", \"2\"], [\"feature_user_age\", \"feature_user_gift_card_balance\"]\n", + ")" ] }, { @@ -770,8 +784,12 @@ "metadata": {}, "outputs": [], "source": [ - "feathr_client.register_features()\n", - "feathr_client.list_registered_features(project_name=f\"{FEATHR_PROJECT_NAME}\")" + "try:\n", + " client.register_features()\n", + "except KeyError:\n", + " # TODO temporarily go around the \"Already exists\" error\n", + " pass\n", + "print(client.list_registered_features(project_name=PROJECT_NAME))" ] }, { @@ -787,7 +805,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3.8.13 ('feathrtest')", + "display_name": "feathr", "language": "python", "name": "python3" }, @@ -801,11 +819,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.13" + "version": "3.10.8 (main, Nov 24 2022, 14:13:03) [GCC 11.2.0]" }, "vscode": { "interpreter": { - "hash": "96bbbb728c64ae5eda27ed1c89d74908bf0652fd45caa45cd0ade6bdc0df4d48" + "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" } } }, diff --git a/docs/samples/product_recommendation_demo_advanced.ipynb b/docs/samples/product_recommendation_demo_advanced.ipynb index 60c2b3b1b..20547f5a1 100644 --- a/docs/samples/product_recommendation_demo_advanced.ipynb +++ b/docs/samples/product_recommendation_demo_advanced.ipynb @@ -276,6 +276,7 @@ "# Redis password\n", "if 'REDIS_PASSWORD' not in os.environ:\n", " from azure.keyvault.secrets import SecretClient\n", + " \n", " vault_url = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", " secret_client = SecretClient(vault_url=vault_url, credential=credential)\n", " retrieved_secret = secret_client.get_secret('FEATHR-ONLINE-STORE-CONN').value\n", @@ -1025,15 +1026,6 @@ "We can push the generated features to the online store like below:" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pd.read_csv(user_profile_file_path).head()" - ] - }, { "cell_type": "code", "execution_count": null, @@ -1060,13 +1052,6 @@ "client.wait_job_to_finish(timeout_sec=5000)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "markdown", "metadata": { @@ -1236,6 +1221,13 @@ " pass\n", " print(client.list_registered_features(project_name=PROJECT_NAME))" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -1264,11 +1256,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.8 (main, Nov 24 2022, 14:13:03) [GCC 11.2.0]" }, "vscode": { "interpreter": { - "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" + "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" } } }, diff --git a/feathr_project/setup.py b/feathr_project/setup.py index 85716733b..660343f07 100644 --- a/feathr_project/setup.py +++ b/feathr_project/setup.py @@ -35,6 +35,7 @@ "pytest-mock>=3.8.1", ], notebook=[ + "azure-cli==2.37.0", "jupyter>=1.0.0", "matplotlib>=3.6.1", "papermill>=2.1.2,<3", # to test run notebooks From b13733bcdf4e555b2d7186ae6b7e42c0a378bea6 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Mon, 19 Dec 2022 14:25:30 -0800 Subject: [PATCH 10/22] Update data url constants to point the source github repo's raw files Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- feathr_project/feathr/datasets/constants.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/feathr_project/feathr/datasets/constants.py b/feathr_project/feathr/datasets/constants.py index 91230d8da..5b9cd08ac 100644 --- a/feathr_project/feathr/datasets/constants.py +++ b/feathr_project/feathr/datasets/constants.py @@ -7,15 +7,15 @@ # Fraud detection sample datasets. # Ref: https://github.com/microsoft/r-server-fraud-detection FRAUD_DETECTION_ACCOUNT_INFO_URL = ( - "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/frauddetection/Account_Info.csv" + "https://raw.github.com/microsoft/r-server-fraud-detection/master/Data/Account_Info.csv" ) FRAUD_DETECTION_FRAUD_TRANSACTIONS_URL = ( - "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/frauddetection/Fraud_Transactions.csv" + "https://raw.github.com/microsoft/r-server-fraud-detection/master/Data/Fraud_Transactions.csv" ) FRAUD_DETECTION_UNTAGGED_TRANSACTIONS_URL = ( - "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/frauddetection/Untagged_Transactions.csv" + "https://raw.github.com/microsoft/r-server-fraud-detection/master/Data/Untagged_Transactions.csv" ) # Product recommendation sample datasets. @@ -39,5 +39,5 @@ # Hotel review sample datasets. # Ref: https://www.kaggle.com/datasets/datafiniti/hotel-reviews HOTEL_REVIEWS_URL = ( - "https://azurefeathrstorage.blob.core.windows.net/public/sample_data/hotel_reviews_100_with_id.csv" + "https://raw.github.com/Azure-Samples/azure-search-python-samples/main/AzureML-Custom-Skill/datasets/hotel_reviews_1000.csv" ) From bb0372d32dc5d5aa8887b25533a426e9c59b6f3d Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Mon, 19 Dec 2022 14:33:05 -0800 Subject: [PATCH 11/22] add dataset url constants to init.py Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- feathr_project/feathr/datasets/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/feathr_project/feathr/datasets/__init__.py b/feathr_project/feathr/datasets/__init__.py index a1e2e5bf3..a6c4d2400 100644 --- a/feathr_project/feathr/datasets/__init__.py +++ b/feathr_project/feathr/datasets/__init__.py @@ -1,9 +1,25 @@ """Utilities for downloading sample datasets""" from feathr.datasets.constants import ( - NYC_TAXI_SMALL_URL + NYC_TAXI_SMALL_URL, + FRAUD_DETECTION_ACCOUNT_INFO_URL, + FRAUD_DETECTION_FRAUD_TRANSACTIONS_URL, + FRAUD_DETECTION_UNTAGGED_TRANSACTIONS_URL, + PRODUCT_RECOMMENDATION_USER_OBSERVATION_URL, + PRODUCT_RECOMMENDATION_USER_PROFILE_URL, + PRODUCT_RECOMMENDATION_USER_PURCHASE_HISTORY_URL, + PRODUCT_RECOMMENDATION_PRODUCT_DETAIL_URL, + HOTEL_REVIEWS_URL, ) __all__ = [ "NYC_TAXI_SMALL_URL", + "FRAUD_DETECTION_ACCOUNT_INFO_URL", + "FRAUD_DETECTION_FRAUD_TRANSACTIONS_URL", + "FRAUD_DETECTION_UNTAGGED_TRANSACTIONS_URL", + "PRODUCT_RECOMMENDATION_USER_OBSERVATION_URL", + "PRODUCT_RECOMMENDATION_USER_PROFILE_URL", + "PRODUCT_RECOMMENDATION_USER_PURCHASE_HISTORY_URL", + "PRODUCT_RECOMMENDATION_PRODUCT_DETAIL_URL", + "HOTEL_REVIEWS_URL", ] From 94507adda1fa96161e029e85f86f0bce01066a13 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Mon, 19 Dec 2022 23:52:29 +0000 Subject: [PATCH 12/22] Update feature embedding notebook to use the original dataset from azure example github Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/feature_embedding.ipynb | 1608 +++++++++++++------------- 1 file changed, 821 insertions(+), 787 deletions(-) diff --git a/docs/samples/feature_embedding.ipynb b/docs/samples/feature_embedding.ipynb index 9868e0879..a85013cdd 100755 --- a/docs/samples/feature_embedding.ipynb +++ b/docs/samples/feature_embedding.ipynb @@ -1,790 +1,824 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using Feature Embedding with Feathr Feature Store\n", - "\n", - "Feature embedding is a way to translate a high-dimensional feature vector to a lower-dimensional vector, where the embedding can be learned and reused across models. In this example, we show how one can define feature embeddings in Feathr Feature Store via **UDF (User Defined Function).**\n", - "\n", - "We use a sample hotel review dataset downloaded from [Azure-Samples repository](https://github.com/Azure-Samples/azure-search-python-samples/tree/main/AzureML-Custom-Skill/datasets). The original dataset can be found [here](https://www.kaggle.com/datasets/datafiniti/hotel-reviews).\n", - "\n", - "For the embedding, a pre-trained [HuggingFace Transformer model](https://huggingface.co/sentence-transformers) is used to encode texts into numerical values. The text embeddings can be used for many NLP problems such as detecting fake reviews, sentiment analysis, and finding similar hotels, but building such models is out of scope and thus we don't cover that in this notebook.\n", - "\n", - "## Prerequisite\n", - "* Databricks: In this notebook, we use Databricks as the target Spark platform.\n", - " - You may use Azure Synapse Spark pool too by following [this](https://github.com/feathr-ai/feathr/blob/main/docs/quickstart_synapse.md) instructions. Note, you'll need to install a `sentence-transformers` pip package to your Spark pool to use the embedding example.\n", - "* Feature registry: We showcase using feature registry later in this notebook. You may use [ARM-template](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-arm.html) to deploy the necessary resources.\n", - "\n", - "First, install Feathr and other necessary packages to run this notebook." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Uncomment and run this cell to install feathr from the latest codes in the repo along with the other necessary packages to run this notebook.\n", - "# !pip install \"git+https://github.com/feathr-ai/feathr#subdirectory=feathr_project\" scikit-learn plotly" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "79bd243c-f78e-4184-82b8-94eb8bea361f", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "import json\n", - "\n", - "import pandas as pd\n", - "from pyspark.sql import DataFrame\n", - "\n", - "import feathr\n", - "from feathr import (\n", - " # dtype\n", - " FLOAT_VECTOR, ValueType,\n", - " # source\n", - " HdfsSource,\n", - " # client\n", - " FeathrClient,\n", - " # feature\n", - " Feature,\n", - " # anchor\n", - " FeatureAnchor,\n", - " # typed_key\n", - " TypedKey,\n", - " # query_feature_list\n", - " FeatureQuery,\n", - " # settings\n", - " ObservationSettings,\n", - " # feathr_configurations\n", - " SparkExecutionConfiguration,\n", - ")\n", - "from feathr.datasets.constants import HOTEL_REVIEWS_URL\n", - "from feathr.datasets.utils import maybe_download\n", - "from feathr.utils.config import DEFAULT_DATABRICKS_CLUSTER_CONFIG, generate_config\n", - "from feathr.utils.job_utils import get_result_df\n", - "from feathr.utils.platform import is_jupyter, is_databricks\n", - "\n", - "print(f\"Feathr version: {feathr.__version__}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Notebook parameters:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "dc33b9b9-d7a2-4fc0-a6c6-fb8a60da3de4", - "showTitle": false, - "title": "" - }, - "tags": [ - "parameters" - ] - }, - "outputs": [], - "source": [ - "RESOURCE_PREFIX = None # TODO fill the value\n", - "PROJECT_NAME = \"hotel_reviews_embedding\"\n", - "\n", - "REGISTRY_ENDPOINT = f\"https://{RESOURCE_PREFIX}webapp.azurewebsites.net/api/v1\"\n", - "\n", - "# TODO fill values to use databricks cluster:\n", - "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", - "if is_databricks():\n", - " # If this notebook is running on Databricks, its context can be used to retrieve token and instance URL\n", - " ctx = dbutils.notebook.entry_point.getDbutils().notebook().getContext()\n", - " DATABRICKS_WORKSPACE_TOKEN_VALUE = ctx.apiToken().get()\n", - " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = f\"https://{ctx.tags().get('browserHostName').get()}\"\n", - "else:\n", - " DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", - " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = None # Set Databricks workspace url to use databricks\n", - "\n", - "# We'll need an authentication credential to access Azure resources and register features \n", - "USE_CLI_AUTH = False # Set True to use interactive authentication\n", - "\n", - "# If set True, register the features to Feathr registry.\n", - "REGISTER_FEATURES = False\n", - "\n", - "# TODO fill the values to use EnvironmentCredential for authentication. (e.g. to run this notebook on DataBricks.)\n", - "AZURE_TENANT_ID = None\n", - "AZURE_CLIENT_ID = None\n", - "AZURE_CLIENT_SECRET = None" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Get an authentication credential to access Azure resources and register features\n", - "if USE_CLI_AUTH:\n", - " # Use AZ CLI interactive browser authentication\n", - " !az login --use-device-code\n", - " from azure.identity import AzureCliCredential\n", - " credential = AzureCliCredential(additionally_allowed_tenants=['*'],)\n", - "elif AZURE_TENANT_ID and AZURE_CLIENT_ID and AZURE_CLIENT_SECRET:\n", - " # Use Environment variable secret\n", - " import os\n", - " from azure.identity import EnvironmentCredential\n", - " os.environ[\"AZURE_TENANT_ID\"] = AZURE_TENANT_ID\n", - " os.environ[\"AZURE_CLIENT_ID\"] = AZURE_CLIENT_ID\n", - " os.environ[\"AZURE_CLIENT_SECRET\"] = AZURE_CLIENT_SECRET\n", - " credential = EnvironmentCredential()\n", - "else:\n", - " # Try to use the default credential\n", - " from azure.identity import DefaultAzureCredential\n", - " credential = DefaultAzureCredential(\n", - " exclude_interactive_browser_credential=False,\n", - " additionally_allowed_tenants=['*'],\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "b91b6f48-87a6-4788-9c09-b8aeb4406c54", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Prepare Dataset\n", - "\n", - "First, prepare the hotel review dataset." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Use dbfs if the notebook is running on Databricks\n", - "if is_databricks():\n", - " WORKING_DIR = f\"/dbfs/{PROJECT_NAME}\"\n", - "else:\n", - " WORKING_DIR = PROJECT_NAME" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "a10a4625-6f98-42cb-9967-3d5d0b75fb7a", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "data_filepath = f\"{WORKING_DIR}/hotel_reviews_100_with_id.csv\"\n", - "maybe_download(src_url=HOTEL_REVIEWS_URL, dst_filepath=data_filepath)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "22e27778-3472-44b7-90e0-aca7d78dbbdc", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "# Verify the data\n", - "pd.read_csv(data_filepath).head(5)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "45c08e6e-a2f7-4ae7-9c3f-81edc1adcf48", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Initialize Feathr Client" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "a8da762c-d245-4f90-abe8-42d4f6a4ea80", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "databricks_config = {\n", - " \"run_name\": \"FEATHR_FILL_IN\",\n", - " \"libraries\": [\n", - " {\"jar\": \"FEATHR_FILL_IN\"},\n", - " # sentence-transformers pip package\n", - " {\"pypi\": {\"package\": \"sentence-transformers\"}},\n", - " ],\n", - " \"spark_jar_task\": {\n", - " \"main_class_name\": \"FEATHR_FILL_IN\",\n", - " \"parameters\": [\"FEATHR_FILL_IN\"],\n", - " },\n", - " \"new_cluster\": DEFAULT_DATABRICKS_CLUSTER_CONFIG,\n", - "}\n", - "\n", - "config_path = generate_config(\n", - " resource_prefix=RESOURCE_PREFIX,\n", - " project_name=PROJECT_NAME,\n", - " spark_config__spark_cluster=\"databricks\",\n", - " # You may set an existing cluster id here, but Databricks recommend to use new clusters for greater reliability.\n", - " databricks_cluster_id=None, # Set None to create a new job cluster\n", - " databricks_workspace_token_value=DATABRICKS_WORKSPACE_TOKEN_VALUE,\n", - " spark_config__databricks__work_dir=f\"dbfs:/{PROJECT_NAME}\",\n", - " spark_config__databricks__workspace_instance_url=SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL,\n", - " spark_config__databricks__config_template=json.dumps(databricks_config),\n", - " feature_registry__api_endpoint=REGISTRY_ENDPOINT,\n", - " use_env_vars=False,\n", - ")\n", - "\n", - "with open(config_path, \"r\") as f:\n", - " print(f.read())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "a35d5b78-542d-4c9e-a64c-76d045a8f587", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "client = FeathrClient(\n", - " config_path=config_path,\n", - " credential=credential,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "352bd8b2-1626-4aee-9b00-58750ac18086", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Feature Creator Scenario\n", - "\n", - "With the feature creator's point of view, we implement a feature embedding UDF, define the embedding output as a feature, and register the feature to Feathr registry. \n", - "\n", - "### Create Features\n", - "\n", - "First, we set the data source path that our feature definition will use. This path will be used from the **Feature Consumer Scenario** later in this notebook when extracting the feature vectors." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if client.spark_runtime == \"local\":\n", - " data_source_path = data_filepath\n", - "# If the notebook is running on Databricks, convert to spark path format\n", - "elif client.spark_runtime == \"databricks\" and is_databricks():\n", - " data_source_path = data_filepath.replace(\"/dbfs\", \"dbfs:\")\n", - "# Otherwise, upload the local file to the cloud storage (either dbfs or adls).\n", - "else:\n", - " data_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(data_filepath)\n", - "\n", - "data_source_path" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create feature embedding UDF. Here, we will use a [pretrained Transformer model from HuggingFace](https://huggingface.co/sentence-transformers/paraphrase-MiniLM-L6-v2)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "cbf14644-fd42-49a2-9199-6471b719e03e", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "def sentence_embedding(df: DataFrame) -> DataFrame:\n", - " \"\"\"Feathr data source UDF to generate sentence embeddings.\n", - "\n", - " Args:\n", - " df: A Spark DataFrame with a column named \"reviews_text\" of type string.\n", - " \n", - " Returns:\n", - " A Spark DataFrame with a column named \"reviews_text_embedding\" of type array.\n", - " \"\"\"\n", - " import pandas as pd\n", - " from pyspark.sql.functions import col, pandas_udf\n", - " from pyspark.sql.types import ArrayType, FloatType\n", - " from sentence_transformers import SentenceTransformer\n", - " \n", - " @pandas_udf(ArrayType(FloatType()))\n", - " def predict_batch_udf(data: pd.Series) -> pd.Series:\n", - " \"\"\"Pandas UDF transforming a pandas.Series of text into a pandas.Series of embeddings.\n", - " You may use iterator input and output instead, e.g. Iterator[pd.Series] -> Iterator[pd.Series]\n", - " \"\"\"\n", - " model = SentenceTransformer('paraphrase-MiniLM-L6-v2')\n", - " embedding = model.encode(data.to_list())\n", - " return pd.Series(embedding.tolist())\n", - "\n", - " return df.withColumn(\"reviews_text_embedding\", predict_batch_udf(col(\"reviews_text\")))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "d570545a-ba3e-4562-9893-a0de8d06e467", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "hdfs_source = HdfsSource(\n", - " name=\"hotel_reviews\",\n", - " path=data_source_path,\n", - " preprocessing=sentence_embedding,\n", - ")\n", - "\n", - "# key is required for the features from non-INPUT_CONTEXT source\n", - "key = TypedKey(\n", - " key_column=\"reviews_id\",\n", - " key_column_type=ValueType.INT64,\n", - " description=\"Reviews ID\",\n", - " full_name=f\"{PROJECT_NAME}.review_id\",\n", - ")\n", - "\n", - "# The column 'reviews_text_embedding' will be generated by our UDF `sentence_embedding`.\n", - "# We use the column as the feature. \n", - "features = [\n", - " Feature(\n", - " name=\"f_reviews_text_embedding\",\n", - " key=key,\n", - " feature_type=FLOAT_VECTOR,\n", - " transform=\"reviews_text_embedding\",\n", - " ),\n", - "]\n", - "\n", - "feature_anchor = FeatureAnchor(\n", - " name=\"feature_anchor\",\n", - " source=hdfs_source,\n", - " features=features,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "75ad69ff-0c94-4cc7-be9e-3cf8f372ecf2", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "client.build_features(\n", - " anchor_list=[feature_anchor],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "d71dd42f-57b3-4ff5-a79f-f154efd3d806", - "showTitle": false, - "title": "" - } - }, - "source": [ - "### Register the Features" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "be389daa-3762-445b-a16a-38f30eb7d7bb", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "if REGISTER_FEATURES:\n", - " try:\n", - " client.register_features()\n", - " except KeyError:\n", - " # TODO temporarily go around the \"Already exists\" error -- \"KeyError: 'guid'\"\n", - " pass \n", - "\n", - " print(client.list_registered_features(project_name=PROJECT_NAME))\n", - " # You can get the actual features too by calling client.get_features_from_registry(PROJECT_NAME)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "195a2a99-98f7-43a5-bd4a-2d65772c93da", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Feature Consumer Scenario\n", - "\n", - "From the feature consumer point of view, we first get the registered feature and then extract the feature vectors by using the feature definition.\n", - "\n", - "### Get Registered Features" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "13a20076-1b24-4537-8d07-a5bf5b440cf0", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "if REGISTER_FEATURES:\n", - " registered_features = client.get_features_from_registry(project_name=PROJECT_NAME)\n", - "else:\n", - " # Assume we get the registered features. This is for a notebook unit-test w/o the actual registration.\n", - " registered_features = {feat.name: feat for feat in features}\n", - "\n", - "print(\"Features:\")\n", - "for f_name, f in registered_features.items():\n", - " print(f\"\\t{f_name} (key: {f.key[0].key_column})\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "7ca62c78-281a-4a84-a8a0-1879ea441e9d", - "showTitle": false, - "title": "" - } - }, - "source": [ - "### Extract the Features" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "c92708e6-ca44-48b6-ae47-30db88e39277", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "feature_name = \"f_reviews_text_embedding\"\n", - "feature_key = registered_features[feature_name].key[0]\n", - "\n", - "if client.spark_runtime == \"databricks\":\n", - " output_filepath = f\"dbfs:/{PROJECT_NAME}/feature_embeddings.parquet\"\n", - "else:\n", - " raise ValueError(\"This notebook is expected to use Databricks as a target Spark cluster.\\\n", - " To use other platforms, you'll need to install `sentence-transformers` pip package to your Spark cluster.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "d9dfe7f6-67d0-407b-aaac-5ac65f9dde3e", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "query = FeatureQuery(\n", - " feature_list=[feature_name],\n", - " key=feature_key,\n", - ")\n", - "\n", - "settings = ObservationSettings(\n", - " observation_path=data_source_path,\n", - ")\n", - "\n", - "client.get_offline_features(\n", - " observation_settings=settings,\n", - " feature_query=query,\n", - " # For more details, see https://feathr-ai.github.io/feathr/how-to-guides/feathr-job-configuration.html\n", - " execution_configurations=SparkExecutionConfiguration({\n", - " \"spark.feathr.outputFormat\": \"parquet\",\n", - " \"spark.sql.execution.arrow.enabled\": \"true\",\n", - " }),\n", - " output_path=output_filepath,\n", - ")\n", - "\n", - "client.wait_job_to_finish(timeout_sec=5000)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "a8be8d73-df8e-40f5-b21a-163e2da4b1c6", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "result_df = get_result_df(client=client, res_url=output_filepath, data_format=\"parquet\")\n", - "result_df[[\"name\", \"reviews_text\", feature_name]].head(5)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's visualize the feature values. Here, we use TSNE (T-distributed Stochastic Neighbor Embedding) using [scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html) to plot the vectors in 2D space." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "c03e4c41-00d7-4163-bdab-b5cf3e22ca30", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "import plotly.graph_objs as go\n", - "from sklearn.manifold import TSNE\n", - "\n", - "\n", - "X = np.stack(result_df[feature_name], axis=0)\n", - "result = TSNE(\n", - " n_components=2,\n", - " init='random',\n", - " perplexity=10,\n", - ").fit_transform(X)\n", - "\n", - "result[:10]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "20a2fe88-3b74-45ad-9b4f-2e63e9171ee1", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "names = set(result_df['name'])\n", - "names" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "25b798da-d0fa-4d37-98a9-a9614c47eb53", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "fig = go.Figure()\n", - "\n", - "for name in names:\n", - " mask = result_df['name']==name\n", - " \n", - " fig.add_trace(go.Scatter(\n", - " x=result[mask, 0],\n", - " y=result[mask, 1],\n", - " name=name,\n", - " textposition='top center',\n", - " mode='markers+text',\n", - " marker={\n", - " 'size': 8,\n", - " 'opacity': 0.8,\n", - " },\n", - " ))\n", - "\n", - "fig.update_layout(\n", - " margin={'l': 0, 'r': 0, 'b': 0, 't': 0},\n", - " showlegend=True,\n", - " autosize=False,\n", - " width=1000,\n", - " height=500,\n", - ")\n", - "fig.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Cleanup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Cleaning up the output files. CAUTION: this maybe dangerous if you \"reused\" the project name.\n", - "import shutil\n", - "shutil.rmtree(WORKING_DIR, ignore_errors=False)" - ] - } - ], - "metadata": { - "application/vnd.databricks.v1+notebook": { - "dashboards": [], - "language": "python", - "notebookMetadata": { - "pythonIndentUnit": 4, - "widgetLayout": [] - }, - "notebookName": "embedding", - "notebookOrigID": 2956141409782062, - "widgets": {} - }, - "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.10 (default, Nov 14 2022, 12:59:47) \n[GCC 9.4.0]" - }, - "vscode": { - "interpreter": { - "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" - } - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using Feature Embedding with Feathr Feature Store\n", + "\n", + "Feature embedding is a way to translate a high-dimensional feature vector to a lower-dimensional vector, where the embedding can be learned and reused across models. In this example, we show how one can define feature embeddings in Feathr Feature Store via **UDF (User Defined Function).**\n", + "\n", + "We use a sample hotel review dataset downloaded from [Azure-Samples repository](https://github.com/Azure-Samples/azure-search-python-samples/tree/main/AzureML-Custom-Skill/datasets). The original dataset can be found [here](https://www.kaggle.com/datasets/datafiniti/hotel-reviews).\n", + "\n", + "For the embedding, a pre-trained [HuggingFace Transformer model](https://huggingface.co/sentence-transformers) is used to encode texts into numerical values. The text embeddings can be used for many NLP problems such as detecting fake reviews, sentiment analysis, and finding similar hotels, but building such models is out of scope and thus we don't cover that in this notebook.\n", + "\n", + "## Prerequisite\n", + "* Databricks: In this notebook, we use Databricks as the target Spark platform.\n", + " - You may use Azure Synapse Spark pool too by following [this](https://github.com/feathr-ai/feathr/blob/main/docs/quickstart_synapse.md) instructions. Note, you'll need to install a `sentence-transformers` pip package to your Spark pool to use the embedding example.\n", + "* Feature registry: We showcase using feature registry later in this notebook. You may use [ARM-template](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-arm.html) to deploy the necessary resources.\n", + "\n", + "First, install Feathr and other necessary packages to run this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment and run this cell to install feathr from the latest codes in the repo along with the other necessary packages to run this notebook.\n", + "# !pip install \"git+https://github.com/feathr-ai/feathr#subdirectory=feathr_project\" scikit-learn plotly" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "79bd243c-f78e-4184-82b8-94eb8bea361f", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "import json\n", + "\n", + "import pandas as pd\n", + "from pyspark.sql import DataFrame\n", + "\n", + "import feathr\n", + "from feathr import (\n", + " # dtype\n", + " FLOAT_VECTOR, ValueType,\n", + " # source\n", + " HdfsSource,\n", + " # client\n", + " FeathrClient,\n", + " # feature\n", + " Feature,\n", + " # anchor\n", + " FeatureAnchor,\n", + " # typed_key\n", + " TypedKey,\n", + " # query_feature_list\n", + " FeatureQuery,\n", + " # settings\n", + " ObservationSettings,\n", + " # feathr_configurations\n", + " SparkExecutionConfiguration,\n", + ")\n", + "from feathr.datasets.constants import HOTEL_REVIEWS_URL\n", + "from feathr.datasets.utils import maybe_download\n", + "from feathr.utils.config import DEFAULT_DATABRICKS_CLUSTER_CONFIG, generate_config\n", + "from feathr.utils.job_utils import get_result_df\n", + "from feathr.utils.platform import is_jupyter, is_databricks\n", + "\n", + "print(f\"Feathr version: {feathr.__version__}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notebook parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "dc33b9b9-d7a2-4fc0-a6c6-fb8a60da3de4", + "showTitle": false, + "title": "" }, - "nbformat": 4, - "nbformat_minor": 0 + "tags": [ + "parameters" + ] + }, + "outputs": [], + "source": [ + "RESOURCE_PREFIX = None # TODO fill the value\n", + "PROJECT_NAME = \"hotel_reviews_embedding\"\n", + "\n", + "REGISTRY_ENDPOINT = f\"https://{RESOURCE_PREFIX}webapp.azurewebsites.net/api/v1\"\n", + "\n", + "# TODO fill values to use databricks cluster:\n", + "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", + "if is_databricks():\n", + " # If this notebook is running on Databricks, its context can be used to retrieve token and instance URL\n", + " ctx = dbutils.notebook.entry_point.getDbutils().notebook().getContext()\n", + " DATABRICKS_WORKSPACE_TOKEN_VALUE = ctx.apiToken().get()\n", + " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = f\"https://{ctx.tags().get('browserHostName').get()}\"\n", + "else:\n", + " DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", + " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = None # Set Databricks workspace url to use databricks\n", + "\n", + "# We'll need an authentication credential to access Azure resources and register features \n", + "USE_CLI_AUTH = False # Set True to use interactive authentication\n", + "\n", + "# If set True, register the features to Feathr registry.\n", + "REGISTER_FEATURES = False\n", + "\n", + "# TODO fill the values to use EnvironmentCredential for authentication. (e.g. to run this notebook on DataBricks.)\n", + "AZURE_TENANT_ID = None\n", + "AZURE_CLIENT_ID = None\n", + "AZURE_CLIENT_SECRET = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get an authentication credential to access Azure resources and register features\n", + "if USE_CLI_AUTH:\n", + " # Use AZ CLI interactive browser authentication\n", + " !az login --use-device-code\n", + " from azure.identity import AzureCliCredential\n", + " credential = AzureCliCredential(additionally_allowed_tenants=['*'],)\n", + "elif AZURE_TENANT_ID and AZURE_CLIENT_ID and AZURE_CLIENT_SECRET:\n", + " # Use Environment variable secret\n", + " import os\n", + " from azure.identity import EnvironmentCredential\n", + " os.environ[\"AZURE_TENANT_ID\"] = AZURE_TENANT_ID\n", + " os.environ[\"AZURE_CLIENT_ID\"] = AZURE_CLIENT_ID\n", + " os.environ[\"AZURE_CLIENT_SECRET\"] = AZURE_CLIENT_SECRET\n", + " credential = EnvironmentCredential()\n", + "else:\n", + " # Try to use the default credential\n", + " from azure.identity import DefaultAzureCredential\n", + " credential = DefaultAzureCredential(\n", + " exclude_interactive_browser_credential=False,\n", + " additionally_allowed_tenants=['*'],\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "b91b6f48-87a6-4788-9c09-b8aeb4406c54", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Prepare Dataset\n", + "\n", + "First, prepare the hotel review dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use dbfs if the notebook is running on Databricks\n", + "if is_databricks():\n", + " WORKING_DIR = f\"/dbfs/{PROJECT_NAME}\"\n", + "else:\n", + " WORKING_DIR = PROJECT_NAME" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "a10a4625-6f98-42cb-9967-3d5d0b75fb7a", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "data_filepath = f\"{WORKING_DIR}/hotel_reviews_100_with_id.csv\"\n", + "maybe_download(src_url=HOTEL_REVIEWS_URL, dst_filepath=data_filepath)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since the review IDs are not included in our sample dataset, we set incremantal numbers to the ID column so that we can use them for feature joinining later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.read_csv(data_filepath)\n", + "df['reviews_id'] = df.index" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "22e27778-3472-44b7-90e0-aca7d78dbbdc", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "# Verify the data\n", + "df.head(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Save the updated data back to file so that we can use it later in this sample notebook.\n", + "df.to_csv(data_filepath, index=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "45c08e6e-a2f7-4ae7-9c3f-81edc1adcf48", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Initialize Feathr Client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "a8da762c-d245-4f90-abe8-42d4f6a4ea80", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "databricks_config = {\n", + " \"run_name\": \"FEATHR_FILL_IN\",\n", + " \"libraries\": [\n", + " {\"jar\": \"FEATHR_FILL_IN\"},\n", + " # sentence-transformers pip package\n", + " {\"pypi\": {\"package\": \"sentence-transformers\"}},\n", + " ],\n", + " \"spark_jar_task\": {\n", + " \"main_class_name\": \"FEATHR_FILL_IN\",\n", + " \"parameters\": [\"FEATHR_FILL_IN\"],\n", + " },\n", + " \"new_cluster\": DEFAULT_DATABRICKS_CLUSTER_CONFIG,\n", + "}\n", + "\n", + "config_path = generate_config(\n", + " resource_prefix=RESOURCE_PREFIX,\n", + " project_name=PROJECT_NAME,\n", + " spark_config__spark_cluster=\"databricks\",\n", + " # You may set an existing cluster id here, but Databricks recommend to use new clusters for greater reliability.\n", + " databricks_cluster_id=None, # Set None to create a new job cluster\n", + " databricks_workspace_token_value=DATABRICKS_WORKSPACE_TOKEN_VALUE,\n", + " spark_config__databricks__work_dir=f\"dbfs:/{PROJECT_NAME}\",\n", + " spark_config__databricks__workspace_instance_url=SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL,\n", + " spark_config__databricks__config_template=json.dumps(databricks_config),\n", + " feature_registry__api_endpoint=REGISTRY_ENDPOINT,\n", + " use_env_vars=False,\n", + ")\n", + "\n", + "with open(config_path, \"r\") as f:\n", + " print(f.read())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "a35d5b78-542d-4c9e-a64c-76d045a8f587", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "client = FeathrClient(\n", + " config_path=config_path,\n", + " credential=credential,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "352bd8b2-1626-4aee-9b00-58750ac18086", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Feature Creator Scenario\n", + "\n", + "With the feature creator's point of view, we implement a feature embedding UDF, define the embedding output as a feature, and register the feature to Feathr registry. \n", + "\n", + "### Create Features\n", + "\n", + "First, we set the data source path that our feature definition will use. This path will be used from the **Feature Consumer Scenario** later in this notebook when extracting the feature vectors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if client.spark_runtime == \"local\":\n", + " data_source_path = data_filepath\n", + "# If the notebook is running on Databricks, convert to spark path format\n", + "elif client.spark_runtime == \"databricks\" and is_databricks():\n", + " data_source_path = data_filepath.replace(\"/dbfs\", \"dbfs:\")\n", + "# Otherwise, upload the local file to the cloud storage (either dbfs or adls).\n", + "else:\n", + " data_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(data_filepath)\n", + "\n", + "data_source_path" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create feature embedding UDF. Here, we will use a [pretrained Transformer model from HuggingFace](https://huggingface.co/sentence-transformers/paraphrase-MiniLM-L6-v2)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "cbf14644-fd42-49a2-9199-6471b719e03e", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "def sentence_embedding(df: DataFrame) -> DataFrame:\n", + " \"\"\"Feathr data source UDF to generate sentence embeddings.\n", + "\n", + " Args:\n", + " df: A Spark DataFrame with a column named \"reviews_text\" of type string.\n", + " \n", + " Returns:\n", + " A Spark DataFrame with a column named \"reviews_text_embedding\" of type array.\n", + " \"\"\"\n", + " import pandas as pd\n", + " from pyspark.sql.functions import col, pandas_udf\n", + " from pyspark.sql.types import ArrayType, FloatType\n", + " from sentence_transformers import SentenceTransformer\n", + " \n", + " @pandas_udf(ArrayType(FloatType()))\n", + " def predict_batch_udf(data: pd.Series) -> pd.Series:\n", + " \"\"\"Pandas UDF transforming a pandas.Series of text into a pandas.Series of embeddings.\n", + " You may use iterator input and output instead, e.g. Iterator[pd.Series] -> Iterator[pd.Series]\n", + " \"\"\"\n", + " model = SentenceTransformer('paraphrase-MiniLM-L6-v2')\n", + " embedding = model.encode(data.to_list())\n", + " return pd.Series(embedding.tolist())\n", + "\n", + " return df.withColumn(\"reviews_text_embedding\", predict_batch_udf(col(\"reviews_text\")))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "d570545a-ba3e-4562-9893-a0de8d06e467", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "hdfs_source = HdfsSource(\n", + " name=\"hotel_reviews\",\n", + " path=data_source_path,\n", + " preprocessing=sentence_embedding,\n", + ")\n", + "\n", + "# key is required for the features from non-INPUT_CONTEXT source\n", + "key = TypedKey(\n", + " key_column=\"reviews_id\",\n", + " key_column_type=ValueType.INT64,\n", + " description=\"Reviews ID\",\n", + " full_name=f\"{PROJECT_NAME}.review_id\",\n", + ")\n", + "\n", + "# The column 'reviews_text_embedding' will be generated by our UDF `sentence_embedding`.\n", + "# We use the column as the feature. \n", + "features = [\n", + " Feature(\n", + " name=\"f_reviews_text_embedding\",\n", + " key=key,\n", + " feature_type=FLOAT_VECTOR,\n", + " transform=\"reviews_text_embedding\",\n", + " ),\n", + "]\n", + "\n", + "feature_anchor = FeatureAnchor(\n", + " name=\"feature_anchor\",\n", + " source=hdfs_source,\n", + " features=features,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "75ad69ff-0c94-4cc7-be9e-3cf8f372ecf2", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "client.build_features(\n", + " anchor_list=[feature_anchor],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "d71dd42f-57b3-4ff5-a79f-f154efd3d806", + "showTitle": false, + "title": "" + } + }, + "source": [ + "### Register the Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "be389daa-3762-445b-a16a-38f30eb7d7bb", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "if REGISTER_FEATURES:\n", + " try:\n", + " client.register_features()\n", + " except KeyError:\n", + " # TODO temporarily go around the \"Already exists\" error -- \"KeyError: 'guid'\"\n", + " pass \n", + "\n", + " print(client.list_registered_features(project_name=PROJECT_NAME))\n", + " # You can get the actual features too by calling client.get_features_from_registry(PROJECT_NAME)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "195a2a99-98f7-43a5-bd4a-2d65772c93da", + "showTitle": false, + "title": "" + } + }, + "source": [ + "## Feature Consumer Scenario\n", + "\n", + "From the feature consumer point of view, we first get the registered feature and then extract the feature vectors by using the feature definition.\n", + "\n", + "### Get Registered Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "13a20076-1b24-4537-8d07-a5bf5b440cf0", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "if REGISTER_FEATURES:\n", + " registered_features = client.get_features_from_registry(project_name=PROJECT_NAME)\n", + "else:\n", + " # Assume we get the registered features. This is for a notebook unit-test w/o the actual registration.\n", + " registered_features = {feat.name: feat for feat in features}\n", + "\n", + "print(\"Features:\")\n", + "for f_name, f in registered_features.items():\n", + " print(f\"\\t{f_name} (key: {f.key[0].key_column})\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "7ca62c78-281a-4a84-a8a0-1879ea441e9d", + "showTitle": false, + "title": "" + } + }, + "source": [ + "### Extract the Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "c92708e6-ca44-48b6-ae47-30db88e39277", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "feature_name = \"f_reviews_text_embedding\"\n", + "feature_key = registered_features[feature_name].key[0]\n", + "\n", + "if client.spark_runtime == \"databricks\":\n", + " output_filepath = f\"dbfs:/{PROJECT_NAME}/feature_embeddings.parquet\"\n", + "else:\n", + " raise ValueError(\"This notebook is expected to use Databricks as a target Spark cluster.\\\n", + " To use other platforms, you'll need to install `sentence-transformers` pip package to your Spark cluster.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "d9dfe7f6-67d0-407b-aaac-5ac65f9dde3e", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "query = FeatureQuery(\n", + " feature_list=[feature_name],\n", + " key=feature_key,\n", + ")\n", + "\n", + "settings = ObservationSettings(\n", + " observation_path=data_source_path,\n", + ")\n", + "\n", + "client.get_offline_features(\n", + " observation_settings=settings,\n", + " feature_query=query,\n", + " # For more details, see https://feathr-ai.github.io/feathr/how-to-guides/feathr-job-configuration.html\n", + " execution_configurations=SparkExecutionConfiguration({\n", + " \"spark.feathr.outputFormat\": \"parquet\",\n", + " \"spark.sql.execution.arrow.enabled\": \"true\",\n", + " }),\n", + " output_path=output_filepath,\n", + ")\n", + "\n", + "client.wait_job_to_finish(timeout_sec=5000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "a8be8d73-df8e-40f5-b21a-163e2da4b1c6", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "result_df = get_result_df(client=client, res_url=output_filepath, data_format=\"parquet\")\n", + "result_df[[\"name\", \"reviews_text\", feature_name]].head(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's visualize the feature values. Here, we use TSNE (T-distributed Stochastic Neighbor Embedding) using [scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.TSNE.html) to plot the vectors in 2D space." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "c03e4c41-00d7-4163-bdab-b5cf3e22ca30", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import plotly.graph_objs as go\n", + "from sklearn.manifold import TSNE\n", + "\n", + "\n", + "X = np.stack(result_df[feature_name], axis=0)\n", + "result = TSNE(\n", + " n_components=2,\n", + " init='random',\n", + " perplexity=10,\n", + ").fit_transform(X)\n", + "\n", + "result[:10]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "20a2fe88-3b74-45ad-9b4f-2e63e9171ee1", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "names = set(result_df['name'])\n", + "names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "25b798da-d0fa-4d37-98a9-a9614c47eb53", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "fig = go.Figure()\n", + "\n", + "for name in names:\n", + " mask = result_df['name']==name\n", + " \n", + " fig.add_trace(go.Scatter(\n", + " x=result[mask, 0],\n", + " y=result[mask, 1],\n", + " name=name,\n", + " textposition='top center',\n", + " mode='markers+text',\n", + " marker={\n", + " 'size': 8,\n", + " 'opacity': 0.8,\n", + " },\n", + " ))\n", + "\n", + "fig.update_layout(\n", + " margin={'l': 0, 'r': 0, 'b': 0, 't': 0},\n", + " showlegend=True,\n", + " autosize=False,\n", + " width=1000,\n", + " height=500,\n", + ")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Cleanup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Cleaning up the output files. CAUTION: this maybe dangerous if you \"reused\" the project name.\n", + "import shutil\n", + "shutil.rmtree(WORKING_DIR, ignore_errors=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "application/vnd.databricks.v1+notebook": { + "dashboards": [], + "language": "python", + "notebookMetadata": { + "pythonIndentUnit": 4, + "widgetLayout": [] + }, + "notebookName": "embedding", + "notebookOrigID": 2956141409782062, + "widgets": {} + }, + "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.10.8" + }, + "vscode": { + "interpreter": { + "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 } From a318cb42ba86e7eb3932216c3bccee90e262e3b7 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Fri, 23 Dec 2022 22:43:10 +0000 Subject: [PATCH 13/22] Add recommendation sample notebook test Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- .../product_recommendation_demo.ipynb | 347 ++++++++------- docs/samples/nyc_taxi_demo.ipynb | 7 +- ...product_recommendation_demo_advanced.ipynb | 409 +++++++++++------- feathr_project/test/samples/test_notebooks.py | 30 ++ 4 files changed, 450 insertions(+), 343 deletions(-) diff --git a/docs/samples/azure_synapse/product_recommendation_demo.ipynb b/docs/samples/azure_synapse/product_recommendation_demo.ipynb index 43912d99a..c96e10adf 100644 --- a/docs/samples/azure_synapse/product_recommendation_demo.ipynb +++ b/docs/samples/azure_synapse/product_recommendation_demo.ipynb @@ -1,35 +1,38 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# Product Recommendation with Feathr on Azure\n", "\n", - "This notebook demonstrates how Feathr Feature Store can simplify and empower your model training and inference. You will learn:\n", + "This notebook demonstrates how Feathr Feature Store can simplify and empower your model training and inference using [Azure Synapse](https://azure.microsoft.com/en-us/products/synapse-analytics/). With this notebook, you will learn the followings:\n", "\n", "1. Define sharable features using Feathr API\n", - "2. Register features with register API.\n", + "2. Register features with register API\n", "3. Create a training dataset via point-in-time feature join with Feathr API\n", "4. Materialize features to online store and then retrieve them with Feathr API\n", "\n", - "In this tutorial, we use Feathr to create a model that predicts users' product rating. " + "In this tutorial, we use Feathr Feature Store to create a model that predicts users' product ratings for an e-commerce website. The main purpose of this notebook is to demonstrate the capabilities running on Azure Synapse and thus we simplified the problem to just predict the ratings for a single product. An advanced example can be found [here](../product_recommendation_demo_advanced.ipynb)." ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "## 1. Prerequisite: Use Azure Resource Manager(ARM) to Provision Azure Resources\n", + "## 1. Prerequisites\n", + "\n", + "### Use Azure Resource Manager (ARM) to Provision Azure Resources for Feathr\n", "\n", - "First step is to provision required cloud resources if you want to use Feathr. Feathr provides a python based client to interact with cloud resources.\n", + "First step is to provision required cloud resources if you want to use Feathr. Feathr provides a python based client to interact with cloud resources. Please follow the steps [here](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-arm.html) to provision required cloud resources. This will create a new resource group and deploy the needed Azure resources in it. \n", "\n", - "Please follow the steps [here](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-arm.html) to provision required cloud resources. This will create a new resource group and deploy the needed Azure resources in it. \n", + "If you already have an existing resource group and only want to install few resources manually you can refer to the cli documentation [here](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-cli.html). It provides CLI commands to install the needed resources.\n", "\n", - "If you already have an existing resource group and only want to install few resources manually you can refer to the cli documentation [here](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-cli.html). It provides CLI commands to install the needed resources. \n", - "**Please Note: CLI documentation is for advance users since there are lot of configurations and role assignment that would have to be done manually so it won't work out of box and should just be used for reference. ARM template is the preferred way to deploy.**\n", + "> Please Note: CLI documentation is for advance users since there are lot of configurations and role assignment that would have to be done manually. Therefore, ARM template is the preferred way to deploy.\n", "\n", - "The below architecture diagram represents how different resources interact with each other\n", + "The below architecture diagram represents how different resources interact with each other.\n", "![Architecture](https://github.com/feathr-ai/feathr/blob/main/docs/images/architecture.png?raw=true)" ] }, @@ -38,9 +41,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2. Prerequisite: Set the required permissions\n", + "### Set the required permissions\n", "\n", - "Before you proceed further, you would need additional permissions: permission to access the keyvault, permission to access the Storage Blob as a Contributor and permission to submit jobs to Synapse cluster. Run the following lines of command in the [Cloud Shell](https://shell.azure.com) before running the cells below. Please replace the resource_prefix with the prefix you used in ARM template deployment.\n", + "Before you proceed further, you would need additional permissions to:\n", + "* Access the keyvault,\n", + "* Access the Storage Blob as a Contributor, and\n", + "* Submit jobs to Synapse cluster.\n", + "\n", + "Run the following commands in the [Cloud Shell](https://shell.azure.com) before moving forward. Please replace `YOUR_RESOURCE_PREFIX` with the value you used in ARM template deployment.\n", "\n", "```\n", " resource_prefix=\"YOUR_RESOURCE_PREFIX\"\n", @@ -59,15 +67,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3. Prerequisite: Install Feathr and it's dependencies and Login to Azure" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Install Feathr and dependencies to run this notebook." + "### Install Feathr python package and it's dependencies\n", + "\n", + "Here, we install the package from the repository's main branch. To use the latest release, you may run `pip install feathr[notebook]` instead. If so, however, some of the new features in this notebook might not work." ] }, { @@ -84,23 +86,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "If you meet errors like 'cannot import FeatherClient from feathr', it may be caused by incompatible version of 'aiohttp'. Please try to install/upgrade it by running the following command:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install aiohttp==3.8.3" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Import Dependencies to make sure everything is installed correctly" + "## 2. Config Feathr Client" ] }, { @@ -116,7 +102,6 @@ "from math import sqrt\n", "\n", "from azure.identity import AzureCliCredential\n", - "from azure.keyvault.secrets import SecretClient\n", " \n", "import pandas as pd\n", "from pyspark.sql import DataFrame\n", @@ -137,22 +122,46 @@ " PRODUCT_RECOMMENDATION_USER_OBSERVATION_URL,\n", " PRODUCT_RECOMMENDATION_USER_PROFILE_URL,\n", " PRODUCT_RECOMMENDATION_USER_PURCHASE_HISTORY_URL,\n", - " PRODUCT_RECOMMENDATION_PRODUCT_DETAIL_URL,\n", ")\n", "from feathr.datasets.utils import maybe_download\n", "from feathr.utils.config import generate_config\n", "from feathr.utils.job_utils import get_result_df\n", - "from feathr.utils.platform import is_databricks\n", "\n", "\n", "print(f\"Feathr version: {feathr.__version__}\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO fill the following values\n", + "RESOURCE_PREFIX = None # The prefix value used at the ARM deployment step\n", + "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", + "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", + "\n", + "PROJECT_NAME = \"product_recommendation_synapse_demo\"\n", + "SPARK_CLUSTER = \"azure_synapse\"\n", + "\n", + "# TODO if you deployed resources manually using different names, you'll need to change the following values accordingly: \n", + "ADLS_ACCOUNT = f\"{RESOURCE_PREFIX}dls\"\n", + "ADLS_FS_NAME = f\"{RESOURCE_PREFIX}fs\"\n", + "AZURE_SYNAPSE_URL = f\"https://{RESOURCE_PREFIX}syws.dev.azuresynapse.net\" # Set Azure Synapse workspace url to use Azure Synapse\n", + "KEY_VAULT_URI = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", + "REDIS_HOST = f\"{RESOURCE_PREFIX}redis.redis.cache.windows.net\"\n", + "REGISTRY_ENDPOINT = f\"https://{RESOURCE_PREFIX}webapp.azurewebsites.net/api/v1\"\n", + "AZURE_SYNAPSE_WORKING_DIR = f\"abfss://{ADLS_FS_NAME}@{ADLS_ACCOUNT}.dfs.core.windows.net/{PROJECT_NAME}\"\n" + ] + }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ + "### Get Azure CLI credential to access Azure resources\n", + "\n", "Login to Azure with a device code (You will see instructions in the output once you execute the cell):" ] }, @@ -174,49 +183,13 @@ "credential = AzureCliCredential(additionally_allowed_tenants=['*'])" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you run into issues where Key vault or other resources are not found through notebook despite being there, make sure you are connected to the right subscription by running the command: 'az account show' and 'az account set --subscription '" - ] - }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "# Feathr Configuration\n", - "\n", - "## Setting the environment variables\n", - "Set the environment variables that will be used by Feathr as configuration. Feathr supports configuration via enviroment variables and yaml, you can read more about it [here](https://feathr-ai.github.io/feathr/how-to-guides/feathr-configuration-and-env.html).\n", - "\n", - "**Fill in the `resource_prefix` that you used while provisioning the resources in Step 1 using ARM.**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# TODO fill the following values\n", - "RESOURCE_PREFIX = None # The prefix value used at the ARM deployment step\n", - "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", - "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", - "\n", - "PROJECT_NAME = \"product_recommendation_synapse_demo\"\n", - "SPARK_CLUSTER = \"azure_synapse\"\n", - "\n", - "# TODO if you deployed resources manually using different names, you'll need to change the following values accordingly: \n", - "ADLS_ACCOUNT=f\"{RESOURCE_PREFIX}dls\"\n", - "ADLS_FS_NAME=f\"{RESOURCE_PREFIX}fs\"\n", - "AZURE_SYNAPSE_URL = f\"https://{RESOURCE_PREFIX}syws.dev.azuresynapse.net\" # Set Azure Synapse workspace url to use Azure Synapse\n", - "KEY_VAULT_URI = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", - "REDIS_HOST = f\"{RESOURCE_PREFIX}redis.redis.cache.windows.net\"\n", - "REGISTRY_ENDPOINT = f\"https://{RESOURCE_PREFIX}webapp.azurewebsites.net/api/v1\"\n", - "\n", - "WORKING_DIR = f\"abfss://{ADLS_FS_NAME}@{ADLS_ACCOUNT}.dfs.core.windows.net/{PROJECT_NAME}\"\n" + "### Set the environment variables\n", + "Set the environment variables that will be used by Feathr as configuration. Feathr supports configuration via enviroment variables and yaml. You can read more about it [here](https://feathr-ai.github.io/feathr/how-to-guides/feathr-configuration-and-env.html)." ] }, { @@ -236,18 +209,29 @@ "outputs": [], "source": [ "if \"REDIS_PASSWORD\" not in os.environ:\n", + " from azure.keyvault.secrets import SecretClient\n", + "\n", " secret_client = SecretClient(vault_url=KEY_VAULT_URI, credential=credential)\n", " retrieved_secret = secret_client.get_secret('FEATHR-ONLINE-STORE-CONN').value\n", " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]" ] }, { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> If you run into issues where Key vault or other resources are not found through notebook despite being there, make sure you are connected to the right subscription by running the command: 'az account show' and 'az account set --subscription '" + ] + }, + { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Write the configuration as yaml file.\n", + "### Generate the Feathr client configuration\n", "\n", - "The code below will write this configuration string to a temporary location and load it to Feathr. Please refer to [feathr_config.yaml](https://github.com/feathr-ai/feathr/blob/main/feathr_project/feathrcli/data/feathr_user_workspace/feathr_config.yaml) for full list of configuration options and details about them." + "The code below will write the onfiguration to a temporary location that will be used by a Feathr client. Please refer to [feathr_config.yaml](https://github.com/feathr-ai/feathr/blob/main/feathr_project/feathrcli/data/feathr_user_workspace/feathr_config.yaml) for full list of configuration options and details." ] }, { @@ -264,7 +248,7 @@ " spark_config__spark_cluster=SPARK_CLUSTER,\n", " spark_config__azure_synapse__dev_url=AZURE_SYNAPSE_URL,\n", " spark_config__azure_synapse__pool_name=AZURE_SYNAPSE_SPARK_POOL,\n", - " spark_config__azure_synapse__workspace_dir=WORKING_DIR,\n", + " spark_config__azure_synapse__workspace_dir=AZURE_SYNAPSE_WORKING_DIR,\n", ")\n", "\n", "with open(config_path, 'r') as f: \n", @@ -272,17 +256,11 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "# Define sharable features using Feathr API\n", - "\n", - "In this tutorial, we use Feathr Feature Store and create a model that predicts users' product rating. To make it simple, let's just predict users' rating for ONE product for an e-commerce website. (We have an [advanced demo](../product_recommendation_demo_advanced.ipynb) that predicts ratings for arbitrary products.)\n", - "\n", - "\n", - "### Initialize Feathr Client\n", - "\n", - "Let's initialize a Feathr client first. The Feathr client provides all the APIs we need to interact with Feathr Feature Store." + "### Initialize Feathr Client" ] }, { @@ -295,11 +273,17 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Understand the Raw Datasets\n", - "We have 3 raw datasets to work with: one observation dataset(a.k.a. label dataset) and two raw datasets to generate features." + "## 3. Define Sharable Features using Feathr API\n", + "\n", + "### Understand raw datasets\n", + "We have three datasets to work with:\n", + "* Observation dataset (a.k.a. labeled dataset)\n", + "* User profile\n", + "* User purchase history" ] }, { @@ -308,7 +292,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Upload datasets into ADLS\n", + "# Upload datasets into ADLS so that Syanpse job can access them\n", "user_observation_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(\n", " PRODUCT_RECOMMENDATION_USER_OBSERVATION_URL\n", ")\n", @@ -326,7 +310,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Observation dataset(a.k.a. label dataset)\n", + "# Observation dataset\n", "# Observation dataset usually comes with a event_timestamp to denote when the observation happened.\n", "# The label here is product_rating. Our model objective is to predict a user's rating for this product.\n", "pd.read_csv(user_observation_source_path).head()" @@ -365,38 +349,41 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### What's a Feature in Feathr\n", + "### What's a feature in Feathr\n", "A feature is an individual measurable property or characteristic of a phenomenon which is sometimes time-sensitive. \n", "\n", - "In Feathr, feature can be defined by the following characteristics:\n", - "1. The typed key (a.k.a. entity id): identifies the subject of feature, e.g. a user id of 123, a product id of SKU234456.\n", - "2. The feature name: the unique identifier of the feature, e.g. user_age, total_spending_in_30_days.\n", - "3. The feature value: the actual value of that aspect at a particular time, e.g. the feature value of the person's age is 30 at year 2022.\n", + "In Feathr, a feature is defined by the following characteristics:\n", + "* The typed key (a.k.a. entity id): identifies the subject of feature, e.g. a user id of 123, a product id of SKU234456.\n", + "* The feature name: the unique identifier of the feature, e.g. user_age, total_spending_in_30_days.\n", + "* The feature value: the actual value of that aspect at a particular time, e.g. the feature value of the person's age is 30 at year 2022.\n", + "* The timestamp: this indicates when the event happened. For example, the user purchased certain product on a certain timestamp. This is usually used for point-in-time join.\n", "\n", - "You can feel that this is defined from a feature consumer(a person who wants to use a feature) perspective. It only tells us what a feature is like. In later sections, you can see how a feature consumer can access the features in a very simple way.\n", + "You can feel that this is defined from a feature consumer (a person who wants to use a feature) perspective. It only tells us what a feature is like. In later sections, you can see how a feature consumer can access the features in a very simple way.\n", "\n", - "To define a feature as well as how it can be produced, additionally we need:\n", - "1. Feature source: what source data that this feature is based on\n", - "2. Transformation: what transformation is used to transform the source data into feature. Transformation can be optional when you just want to take a column out from the source data.\n", + "To define how to produce the feature, we need to specify:\n", + "* Feature source: what source data that this feature is based on\n", + "* Transformation: what transformation is used to transform the source data into feature. Transformation can be optional when you just want to take a column out from the source data.\n", "\n", - "(For more details on feature definition, please refer to the [Feathr Feature Definition Guide](https://feathr-ai.github.io/feathr/concepts/feature-definition.html))" + "(For more details on feature definition, please refer to the [Feathr Feature Definition Guide](https://feathr-ai.github.io/feathr/concepts/feature-definition.html).)" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Define Sources Section with Preprocssing\n", - "A [feature source](https://feathr.readthedocs.io/en/latest/#feathr.Source) defines where to find the source data and how to use the source data for the upcoming feature transformation. There are different types of feature sources that you can use. HdfsSource is the most commonly used one that can connect you to data lake, Snowflake database tables etc. It's simliar to database connector.\n", + "### Define a feature source with preprocessing function\n", + "A [feature source](https://feathr.readthedocs.io/en/latest/#feathr.Source) defines where to find the source data and how to use the source data for the upcoming feature transformation. There are different types of feature sources that you can use. `HdfsSource` is the most commonly used one that can connect you to data lake, Snowflake database tables etc. It's similar to database connector.\n", "\n", - "To define HdfsSource, we need:\n", - "1. `name`: It's used for you to recognize it. It has to be unique among all other feature source. Here we use `userProfileData`. \n", - "2. `path`: It points to the location that we can find the source data.\n", - "3. `preprocessing`(optional): If you want some preprocessing other than provided transformation, you can do it here. This preprocessing will be applied all the transformations of this source.\n", - "4. `event_timestamp_column`(optioanl): there are `event_timestamp_column` and `timestamp_format` used for point-in-time join and we will cover them later.\n", + "We define `HdfsSource` with following arguments:\n", + "* `name`: It's used for you to recognize it. It has to be unique among all other feature source. \n", + "* `path`: It points to the source data.\n", + "* `preprocessing` (optional): Data preprocessing UDF (user defined function). The function will be applied to the data before all the feature transformations based on this source.\n", + "* `event_timestamp_column` (optional): there are `event_timestamp_column` and `timestamp_format` used for point-in-time join (we will cover them later).\n", "\n", "See [the python API documentation](https://feathr.readthedocs.io/en/latest/#feathr.HdfsSource) to get the details of each input fields. " ] @@ -410,8 +397,7 @@ "def feathr_udf_preprocessing(df: DataFrame) -> DataFrame:\n", " from pyspark.sql.functions import col\n", "\n", - " df = df.withColumn(\"tax_rate_decimal\", col(\"tax_rate\") / 100)\n", - " return df\n", + " return df.withColumn(\"tax_rate_decimal\", col(\"tax_rate\") / 100)\n", "\n", "\n", "batch_source = HdfsSource(\n", @@ -422,16 +408,18 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Define Features on Top of Data Sources\n", + "### Define features\n", + "\n", "To define features on top of the `HdfsSource`, we need to:\n", - "1. specify the key of this feature: feature are like other data, they are keyed by some id. For example, user_id, product_id. You can also define compound keys.\n", - "2. specify the name of the feature via `name` parameter and how to transform it from source data via `transform` parameter. Also some other metadata, like `feature_type`.\n", - "3. group them together so we know it's from one `HdfsSource` via `FeatureAnchor`. Also give it a unique name via `name` parameter so we can recognize it.\n", + "1. Specify the key of this feature: feature are like other data, they are keyed by some id. For example, user_id, product_id. You can also define compound keys.\n", + "2. Specify the name of the feature via `name` parameter and how to transform it from source data via `transform` parameter. Also some other metadata, like `feature_type`.\n", + "3. Group them together so we know it's from one `HdfsSource` via `FeatureAnchor`. Also give it a unique name via `name` parameter so we can recognize it.\n", "\n", - "It's called FeatureAnchor since it's like this group of features are anchored to the source. There are other types of features that are computed on top of other features(a.k.a. derived feature which we will cover in next section)" + "It's called **Feature Anchor** since this group of features are anchored to the source. There are other types of features that are computed on top of other features, called `DerivedFeatures` (we will cover this in the next section)." ] }, { @@ -440,7 +428,6 @@ "metadata": {}, "outputs": [], "source": [ - "# Let's define some features for users so our recommendation can be customized for users.\n", "user_id = TypedKey(\n", " key_column=\"user_id\",\n", " key_column_type=ValueType.INT32,\n", @@ -454,18 +441,21 @@ " feature_type=INT32,\n", " transform=\"age\",\n", ")\n", + "\n", "feature_user_tax_rate = Feature(\n", " name=\"feature_user_tax_rate\",\n", " key=user_id,\n", " feature_type=FLOAT,\n", " transform=\"tax_rate_decimal\",\n", ")\n", + "\n", "feature_user_gift_card_balance = Feature(\n", " name=\"feature_user_gift_card_balance\",\n", " key=user_id,\n", " feature_type=FLOAT,\n", " transform=\"gift_card_balance\",\n", ")\n", + "\n", "feature_user_has_valid_credit_card = Feature(\n", " name=\"feature_user_has_valid_credit_card\",\n", " key=user_id,\n", @@ -486,27 +476,26 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Window aggregation features\n", - "\n", - "Using [window aggregations](https://en.wikipedia.org/wiki/Window_function_%28SQL%29) can help us create more powerful features. A window aggregation feature compresses large amount of information into one single feature value. Using our raw data as an example, we have the user's purchase history data that might be quite some rows, we want to create a window aggregation feature that represents their last 90 days of average purchase amount.\n", + "### Define window aggregation features\n", "\n", - "To create this window aggregation feature via Feathr, we just need to define the following parameters with `WindowAggTransformation` API:\n", - "1. `agg_expr`: the field/column you want to aggregate. It can be a ANSI SQL expression. So we just write `cast_float(purchase_amount)`(the raw data might be in string form, let's cast_float).\n", - "2. `agg_func`: the aggregation function you want. We want to use `AVG` here.\n", - "3. `window`: the aggregation window size you want. Let's use `90d`. You can tune your windows to create different window aggregation features.\n", + "[Window aggregation](https://en.wikipedia.org/wiki/Window_function_%28SQL%29) helps us to create more powerful features by compressing large amount of information. For example, we can compute *average purchase amount over the last 90 days* from the purchase history to capture user's recent consumption trend.\n", "\n", - "For window aggregation functions, see the supported fields below:\n", + "To create window aggregation features, we define `WindowAggTransformation` with following arguments:\n", + "1. `agg_expr`: the field/column you want to aggregate. It can be an ANSI SQL expression, e.g. `cast_float(purchase_amount)` to cast `str` type values to `float`.\n", + "2. `agg_func`: the aggregation function, e.g. `AVG`. See below table for the full list of supported functions.\n", + "3. `window`: the aggregation window size, e.g. `90d` to aggregate over the 90 days.\n", "\n", "| Aggregation Type | Input Type | Description |\n", "| --- | --- | --- |\n", - "|SUM, COUNT, MAX, MIN, AVG\t|Numeric|Applies the the numerical operation on the numeric inputs. |\n", - "|MAX_POOLING, MIN_POOLING, AVG_POOLING\t| Numeric Vector | Applies the max/min/avg operation on a per entry bassis for a given a collection of numbers.|\n", - "|LATEST| Any |Returns the latest not-null values from within the defined time window |\n", + "| `SUM`, `COUNT`, `MAX`, `MIN`, `AVG` | Numeric | Applies the the numerical operation on the numeric inputs. |\n", + "| `MAX_POOLING`, `MIN_POOLING`, `AVG_POOLING`\t| Numeric Vector | Applies the max/min/avg operation on a per entry basis for a given a collection of numbers. |\n", + "| `LATEST` | Any | Returns the latest not-null values from within the defined time window. |\n", + "\n", "\n", - "(Note that the `agg_func` should be any of these.)\n", "\n", "After you have defined features and sources, bring them together to build an anchor:" ] @@ -541,13 +530,13 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Derived Features Section\n", - "Derived features are features that are computed from other Feathr features. They could be computed from anchored features, or other derived features.\n", - "\n", - "Typical usage includes feature cross(f1 * f2), or computing cosine similarity between two features. The syntax works in a similar way." + "### Define derived features\n", + "Derived features are the features that are computed from anchored features or other derived features.\n", + "Typical use cases include feature cross (f1 * f2) or cosine similarity between two features." ] }, { @@ -556,21 +545,24 @@ "metadata": {}, "outputs": [], "source": [ - "feature_user_purchasing_power = DerivedFeature(\n", - " name=\"feature_user_purchasing_power\",\n", - " key=user_id,\n", - " feature_type=FLOAT,\n", - " input_features=[feature_user_gift_card_balance, feature_user_has_valid_credit_card],\n", - " transform=\"feature_user_gift_card_balance + if(boolean(feature_user_has_valid_credit_card), 100, 0)\",\n", - ")" + "derived_features = [\n", + " DerivedFeature(\n", + " name=\"feature_user_purchasing_power\",\n", + " key=user_id,\n", + " feature_type=FLOAT,\n", + " input_features=[feature_user_gift_card_balance, feature_user_has_valid_credit_card],\n", + " transform=\"feature_user_gift_card_balance + if(boolean(feature_user_has_valid_credit_card), 100, 0)\",\n", + " )\n", + "]" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Build Features\n", - "Lastly, we need to build these features so that they can be consumed later. Note that we have to build both the \"anchor\" and the \"derived\" features." + "### Build features\n", + "Lastly, we need to build these features so that they can be consumed later." ] }, { @@ -581,33 +573,30 @@ "source": [ "client.build_features(\n", " anchor_list=[user_agg_feature_anchor, user_feature_anchor],\n", - " derived_feature_list=[feature_user_purchasing_power],\n", + " derived_feature_list=derived_features,\n", ")" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Optional: A Special Type of Feature: Request Feature\n", - "Sometimes features defined on top of request data(a.k.a. observation data) may have no entity key or timestamp. It is merely a function/transformation executing against request data at runtime.\n", - "\n", - "For example, the day of the week of the request, which is calculated by converting the request UNIX timestamp. In this case, the `source` section should be `INPUT_CONTEXT` to indicate the source of those defined anchors.\n", + "### Extra topic: Passing-through features\n", "\n", - "We won't cover the details of it in this notebook." + "Some features could be computed from the observation data directly at runtime and thus will not require an entity key or timestamp for joining. For example, *the day of the week of the request*. We can define such features by passing `source=INPUT_CONTEXT` to the anchor. Details about the passing through features can be found from [here](https://feathr-ai.github.io/feathr/concepts/feathr-concepts-for-beginners.html#motivation-on-input_context)." ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "## Create training data using point-in-time correct feature join\n", - "\n", - "A training dataset usually contains `entity id` column(s), multiple `feature` columns, event timestamp column and `label/target` column. \n", + "## 4. Create Training Data using Point-in-Time Correct Feature Join\n", "\n", - "To create a training dataset using Feathr, we need to provide a feature join settings to specify what features and how these features should be joined to the observation data. \n", + "To create a training dataset using Feathr, we need to provide a **feature join settings** to specify what features and how these features should be joined to the observation data. \n", "\n", - "(To learn more on this topic, please refer to [Point-in-time Correctness](https://feathr-ai.github.io/feathr/concepts/point-in-time-join.html))." + "To learn more on this topic, please refer to [Point-in-time Correctness document](https://feathr-ai.github.io/feathr/concepts/point-in-time-join.html)." ] }, { @@ -617,14 +606,7 @@ "outputs": [], "source": [ "user_feature_query = FeatureQuery(\n", - " feature_list=[\n", - " \"feature_user_age\",\n", - " \"feature_user_tax_rate\",\n", - " \"feature_user_gift_card_balance\",\n", - " \"feature_user_has_valid_credit_card\",\n", - " \"feature_user_avg_purchase_for_90days\",\n", - " \"feature_user_purchasing_power\",\n", - " ],\n", + " feature_list=[feat.name for feat in features + agg_features + derived_features],\n", " key=user_id,\n", ")\n", "\n", @@ -633,20 +615,21 @@ " event_timestamp_column=\"event_timestamp\",\n", " timestamp_format=\"yyyy-MM-dd\",\n", ")\n", + "\n", "client.get_offline_features(\n", " observation_settings=settings,\n", " feature_query=[user_feature_query],\n", " output_path=user_profile_source_path.rpartition(\"/\")[0] + f\"/product_recommendation_features.avro\",\n", ")\n", + "\n", "client.wait_job_to_finish(timeout_sec=5000)" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Download the result and show the result\n", - "\n", "Let's use the helper function `get_result_df` to download the result and view it:" ] }, @@ -679,21 +662,26 @@ "from sklearn.model_selection import train_test_split\n", "\n", "\n", + "# Fill None values with 0\n", "final_df = (\n", " res_df\n", " .drop([\"event_timestamp\"], axis=1, errors=\"ignore\")\n", " .fillna(0)\n", ")\n", "\n", + "# Split data into train and test\n", "X_train, X_test, y_train, y_test = train_test_split(\n", " final_df.drop([\"product_rating\"], axis=1),\n", " final_df[\"product_rating\"].astype(\"float64\"),\n", " test_size=0.2,\n", " random_state=42,\n", ")\n", + "\n", + "# Train a prediction model\n", "model = GradientBoostingRegressor()\n", "model.fit(X_train, y_train)\n", "\n", + "# Predict and evaluate\n", "y_pred = model.predict(X_test)\n", "rmse = sqrt(mean_squared_error(y_test.values.flatten(), y_pred))\n", "\n", @@ -701,23 +689,25 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "## Materialize feature value into offline/online storage\n", + "## 5. Feature Materialization\n", "\n", - "In the previous section, we demonstrated how Feathr can compute feature value to generate training dataset from feature definition on-they-fly.\n", + "In the previous section, we demonstrated how Feathr can compute feature value to generate training dataset from feature definition on-they-fly. Now let's talk about how we can use the trained models.\n", "\n", - "Now let's talk about how we can use the trained models. We can use the trained models for both online and offline inference. In both cases, we need features to be fed into the models. For offline inference, you can compute and get the features on-demand; or you can store the computed features to some offline database for later offline inference.\n", + "We can use the trained models for both online and offline inference. In both cases, we need features to be fed into the models. For offline inference, you can compute and get the features on-demand; or you can store the computed features to some offline database for later offline inference.\n", "\n", "For online inference, we can use Feathr to compute and store the features in the online database. Then use it for online inference when the request comes.\n", "\n", "![img](../../images/online_inference.jpg)\n", "\n", - "\n", "In this section, we will focus on materialize features to online store. For materialization to offline store, you can check out our [user guide](https://feathr-ai.github.io/feathr/concepts/materializing-features.html#materializing-features-to-offline-store).\n", "\n", - "We can push the computed features to the online store(Redis) like below:" + "\n", + "### Materialize feature values to online store\n", + "We can push the computed features to the online store (Redis) like below:" ] }, { @@ -740,11 +730,12 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Fetch feature value from online store\n", - "We can then get the features from the online store (Redis) via the client's `get_online_features` or `multi_get_online_features` API." + "### Fetch feature values from online store\n", + "We can then get the features from the online store via the client's `get_online_features` or `multi_get_online_features` API." ] }, { @@ -770,12 +761,12 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ - "### Registering and Fetching features\n", - "\n", - "We can also register the features and share them across teams:" + "## 6. Feature Registration\n", + "Lastly, we can also register the features and share them across teams:" ] }, { @@ -786,18 +777,18 @@ "source": [ "try:\n", " client.register_features()\n", - "except KeyError:\n", - " # TODO temporarily go around the \"Already exists\" error\n", - " pass\n", + "except Exception as e:\n", + " print(e)\n", "print(client.list_registered_features(project_name=PROJECT_NAME))" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n", - "In this notebook you learnt how to set up Feathr and use it to create features, register features and use those features for model training and inferencing.\n", + "In this notebook you learned how to set up Feathr and use it to create features, register the features and use them for model training and inferencing.\n", "\n", "We hope this example gave you a good sense of Feathr's capabilities and how you could leverage it within your organization's MLOps workflow." ] diff --git a/docs/samples/nyc_taxi_demo.ipynb b/docs/samples/nyc_taxi_demo.ipynb index fc553f422..bda920e22 100644 --- a/docs/samples/nyc_taxi_demo.ipynb +++ b/docs/samples/nyc_taxi_demo.ipynb @@ -960,9 +960,8 @@ "if REGISTER_FEATURES:\n", " try:\n", " client.register_features()\n", - " except KeyError:\n", - " # TODO temporarily go around the \"Already exists\" error\n", - " pass \n", + " except Exception as e:\n", + " print(e) \n", " print(client.list_registered_features(project_name=PROJECT_NAME))\n", " # You can get the actual features too by calling client.get_features_from_registry(PROJECT_NAME)" ] @@ -1159,7 +1158,7 @@ }, "vscode": { "interpreter": { - "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" + "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" } } }, diff --git a/docs/samples/product_recommendation_demo_advanced.ipynb b/docs/samples/product_recommendation_demo_advanced.ipynb index 20547f5a1..a0fc34988 100644 --- a/docs/samples/product_recommendation_demo_advanced.ipynb +++ b/docs/samples/product_recommendation_demo_advanced.ipynb @@ -12,30 +12,27 @@ } }, "source": [ - "# Feathr Feature Store on Azure Demo Notebook\n", + "# Product Recommendation with Feathr on Azure (Advanced)\n", "\n", "This notebook illustrates the use of Feathr Feature Store to create a model that predict users' rating for different products for a e-commerce website.\n", "\n", - "## Model Problem Statement\n", + "### Model Problem Statement\n", "The e-commerce website has collected past user ratings for various products. The website also collected data about user and product, like user age, product category etc. Now we want to predict users' product rating for new product so that we can recommend the new product to users that give a high rating for those products.\n", "\n", - "After the model is trained, given a user_id, product_id pair and features, we should be able to predict the product rating that the user will give for this product_id.\n", + "### Feature Creation Illustration\n", + "In this example, our observation data has compound entity key where a record is uniquely identified by `user_id` and `product_id`. With that, we can think about three types of features:\n", + "1. **User features** that are different for different users but are the same for different products. For example, user age is different for different users but it's product-agnostic.\n", + "2. **Product features** that are different for different products but are the same for all the users.\n", + "3. **User-to-product** features that are different for different users AND different products. For example, a feature to represent if the user has bought this product before or not.\n", "\n", - "(Compared with [the beginner version of product recommendation](https://github.com/feathr-ai/feathr/blob/main/docs/samples/azure_synapse/product_recommendation_demo.ipynb), this tutorial expanded the example by predicting ratings for all products.)\n", - "\n", - "## Feature Creation Illustration\n", - "In this example, our observation data has compound entity key where a record is uniquely identified by user_id and product_id. So there might be 3 types of features:\n", - "* User features that are different for different users but are the same for different products. For example, user age is different for different users but it's the same for all products(or it's product-agnostic).\n", - "* Product features that are different for different products but are the same for different users.\n", - "* User-to-product features that are different for different users AND different products. For example, a feature to represent if the user has bought this product before or not.\n", - "\n", - "We will focus on the first two in our example.\n", + "In this example, we will focus on the first two types of features. After we train a model based on those features, we predict the product ratings that users will give for the products.\n", "\n", "The feature creation flow is as below:\n", "![Feature Flow](https://github.com/feathr-ai/feathr/blob/main/docs/images/product_recommendation_advanced.jpg?raw=true)" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -46,16 +43,48 @@ } }, "source": [ - "## Prerequisite: Use Quick Start Template to Provision Azure Resources\n", + "## 1. Prerequisites\n", + "\n", + "### Use Azure Resource Manager (ARM) to Provision Azure Resources for Feathr\n", "\n", "First step is to provision required cloud resources if you want to use Feathr. Feathr provides a python based client to interact with cloud resources.\n", "\n", - "Please follow the steps [here](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-arm.html) to provision required cloud resources. Due to the complexity of the possible cloud environment, it is almost impossible to create a script that works for all the use cases. Because of this, [azure_resource_provision.sh](https://github.com/feathr-ai/feathr/blob/main/docs/how-to-guides/azure_resource_provision.sh) is a full end to end command line to create all the required resources, and you can tailor the script as needed, while [the companion documentation](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-cli.html) can be used as a complete guide for using that shell script. \n", + "Please follow the steps [here](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-arm.html) to provision required cloud resources. Due to the complexity of the possible cloud environment, it is almost impossible to create a script that works for all the use cases. Because of this, [azure_resource_provision.sh](https://github.com/feathr-ai/feathr/blob/main/docs/how-to-guides/azure_resource_provision.sh) is a full end to end command line to create all the required resources, and you can tailor the script as needed, while [the companion documentation](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-cli.html) can be used as a complete guide for using that shell script.\n", + "\n", + "If you already have an existing resource group and only want to install few resources manually you can refer to the cli documentation [here](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-cli.html). It provides CLI commands to install the needed resources.\n", "\n", + "> Please Note: CLI documentation is for advance users since there are lot of configurations and role assignment that would have to be done manually. Therefore, ARM template is the preferred way to deploy.\n", "\n", + "The below architecture diagram represents how different resources interact with each other.\n", "![Architecture](https://github.com/feathr-ai/feathr/blob/main/docs/images/architecture.png?raw=true)" ] }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set the required permissions\n", + "\n", + "Before you proceed further, you would need additional permissions to:\n", + "* Access the keyvault,\n", + "* Access the Storage Blob as a Contributor, and\n", + "* Submit jobs to Synapse cluster.\n", + "\n", + "Run the following commands in the [Cloud Shell](https://shell.azure.com) before moving forward. Please replace `YOUR_RESOURCE_PREFIX` with the value you used in ARM template deployment.\n", + "\n", + "```\n", + " resource_prefix=\"YOUR_RESOURCE_PREFIX\"\n", + " synapse_workspace_name=\"${resource_prefix}syws\"\n", + " keyvault_name=\"${resource_prefix}kv\"\n", + " objectId=$(az ad signed-in-user show --query id -o tsv)\n", + " az keyvault update --name $keyvault_name --enable-rbac-authorization false\n", + " az keyvault set-policy -n $keyvault_name --secret-permissions get list --object-id $objectId\n", + " az role assignment create --assignee $userId --role \"Storage Blob Data Contributor\"\n", + " az synapse role assignment create --workspace-name $synapse_workspace_name --role \"Synapse Contributor\" --assignee $userId\n", + "```\n" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -68,9 +97,9 @@ } }, "source": [ - "## Prerequisite: Install Feathr pip package and notebook dependencies\n", + "### Install Feathr python package and it's dependencies\n", "\n", - "In the first step (Provision cloud resources), you should have provisioned all the required cloud resources. Run the code below to install Feathr" + "Here, we install the package from the repository's main branch. To use the latest release, you may run `pip install feathr[notebook]` instead. If so, however, some of the new features in this notebook might not work." ] }, { @@ -79,7 +108,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Install feathr from the latest codes in the repo. You may use `pip install feathr[notebook]` as well.\n", + "# Uncomment and run this cell to install feathr from the latest codes in the repo. You may use `pip install feathr[notebook]` as well.\n", "# !pip install \"git+https://github.com/feathr-ai/feathr.git#subdirectory=feathr_project&egg=feathr[notebook]\" " ] }, @@ -89,8 +118,16 @@ "metadata": {}, "outputs": [], "source": [ - "%load_ext autoreload\n", - "%autoreload 2" + "# We also install InterpretML package for model explainability\n", + "!pip install interpret" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Config Feathr Client" ] }, { @@ -143,14 +180,6 @@ "print(f\"Feathr version: {feathr.__version__}\")" ] }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "> If you meet errors like 'cannot import FeatherClient from feathr', it may be caused by incompatible version of 'aiohttp'. Please try to install/upgrade it by running: '! pip install -U aiohttp' or '! pip install aiohttp==3.8.3'" - ] - }, { "cell_type": "code", "execution_count": null, @@ -180,9 +209,17 @@ "\n", "# TODO fill values to use Azure Synapse cluster:\n", "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", - "AZURE_SYNAPSE_URL = None # Set Azure Synapse workspace url to use Azure Synapse\n", "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", "\n", + "# TODO if you deployed resources manually using different names, you'll need to change the following values accordingly: \n", + "ADLS_ACCOUNT = f\"{RESOURCE_PREFIX}dls\"\n", + "ADLS_FS_NAME = f\"{RESOURCE_PREFIX}fs\"\n", + "AZURE_SYNAPSE_URL = f\"https://{RESOURCE_PREFIX}syws.dev.azuresynapse.net\" # Set Azure Synapse workspace url to use Azure Synapse\n", + "KEY_VAULT_URI = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", + "REDIS_HOST = f\"{RESOURCE_PREFIX}redis.redis.cache.windows.net\"\n", + "REGISTRY_ENDPOINT = f\"https://{RESOURCE_PREFIX}webapp.azurewebsites.net/api/v1\"\n", + "AZURE_SYNAPSE_WORKING_DIR = f\"abfss://{ADLS_FS_NAME}@{ADLS_ACCOUNT}.dfs.core.windows.net/{PROJECT_NAME}\"\n", + "\n", "# An existing Feathr config file path. If None, we'll generate a new config based on the constants in this cell.\n", "FEATHR_CONFIG_PATH = None\n", "\n", @@ -200,46 +237,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Setup necessary environment variables (Skip if using the above Quick Start Template)\n", - "\n", - "You should setup the environment variables in order to run this sample. More environment variables can be set by referring to [feathr_config.yaml](https://github.com/feathr-ai/feathr/blob/main/feathr_project/feathrcli/data/feathr_user_workspace/feathr_config.yaml) and use that as the source of truth. It also has more explanations on the meaning of each variable.\n", - "\n", - "To run this notebook, for Azure users, you need REDIS_PASSWORD.\n", - "To run this notebook, for Databricks useres, you need DATABRICKS_WORKSPACE_TOKEN_VALUE and REDIS_PASSWORD." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if SPARK_CLUSTER == \"azure_synapse\" and not os.environ.get(\"ADLS_KEY\"):\n", - " os.environ[\"ADLS_KEY\"] = ADLS_KEY\n", - "elif SPARK_CLUSTER == \"databricks\" and not os.environ.get(\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"):\n", - " os.environ[\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"] = DATABRICKS_WORKSPACE_TOKEN_VALUE" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Permission\n", - "\n", - "To proceed with the following steps, you may need additional permission: permission to access the keyvault, permission to access the Storage Blob as a Contributor and permission to submit jobs to Synapse cluster. Skip this step if you have already given yourself the access. Otherwise, run the following lines of command in the Cloud Shell before running the cell below.\n", - "\n", - "```\n", - "userId=\n", - "resource_prefix=\n", - "synapse_workspace_name=\"${resource_prefix}syws\"\n", - "keyvault_name=\"${resource_prefix}kv\"\n", - "objectId=$(az ad user show --id $userId --query id -o tsv)\n", - "az keyvault update --name $keyvault_name --enable-rbac-authorization false\n", - "az keyvault set-policy -n $keyvault_name --secret-permissions get list --object-id $objectId\n", - "az role assignment create --assignee $userId --role \"Storage Blob Data Contributor\"\n", - "az synapse role assignment create --workspace-name $synapse_workspace_name --role \"Synapse Contributor\" --assignee $userId\n", - "```" + "### Get Azure credential to access Azure resources" ] }, { @@ -267,6 +265,27 @@ " )" ] }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Set the environment variables\n", + "Set the environment variables that will be used by Feathr as configuration. Feathr supports configuration via enviroment variables and yaml. You can read more about it [here](https://feathr-ai.github.io/feathr/how-to-guides/feathr-configuration-and-env.html)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if SPARK_CLUSTER == \"azure_synapse\" and not os.environ.get(\"ADLS_KEY\"):\n", + " os.environ[\"ADLS_KEY\"] = ADLS_KEY\n", + "elif SPARK_CLUSTER == \"databricks\" and not os.environ.get(\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"):\n", + " os.environ[\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"] = DATABRICKS_WORKSPACE_TOKEN_VALUE" + ] + }, { "cell_type": "code", "execution_count": null, @@ -277,12 +296,19 @@ "if 'REDIS_PASSWORD' not in os.environ:\n", " from azure.keyvault.secrets import SecretClient\n", " \n", - " vault_url = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", - " secret_client = SecretClient(vault_url=vault_url, credential=credential)\n", + " secret_client = SecretClient(vault_url=KEY_VAULT_URI, credential=credential)\n", " retrieved_secret = secret_client.get_secret('FEATHR-ONLINE-STORE-CONN').value\n", " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]" ] }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> If you run into issues where Key vault or other resources are not found through notebook despite being there, make sure you are connected to the right subscription by running the command: 'az account show' and 'az account set --subscription '" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -295,11 +321,9 @@ } }, "source": [ - "## Prerequisite: Configure the required environment (Don't need to update if using the above Quick Start Template)\n", - "\n", - "In the first step (Provision cloud resources), you should have provisioned all the required cloud resources. If you use Feathr CLI to create a workspace, you should have a folder with a file called `feathr_config.yaml` in it with all the required configurations. Otherwise, update the configuration below.\n", + "### Generate the Feathr client configuration\n", "\n", - "The code below will write this configuration string to a temporary location so that Feathr client can load it. Please refer to [feathr_config.yaml](https://github.com/feathr-ai/feathr/blob/main/feathr_project/feathrcli/data/feathr_user_workspace/feathr_config.yaml) for more details." + "The code below will write the onfiguration to a temporary location that will be used by a Feathr client. Please refer to [feathr_config.yaml](https://github.com/feathr-ai/feathr/blob/main/feathr_project/feathrcli/data/feathr_user_workspace/feathr_config.yaml) for full list of configuration options and details." ] }, { @@ -321,9 +345,12 @@ " config_path = generate_config(\n", " resource_prefix=RESOURCE_PREFIX,\n", " project_name=PROJECT_NAME,\n", + " online_store__redis__host=REDIS_HOST,\n", + " feature_registry__api_endpoint=REGISTRY_ENDPOINT,\n", " spark_config__spark_cluster=SPARK_CLUSTER,\n", " spark_config__azure_synapse__dev_url=AZURE_SYNAPSE_URL,\n", " spark_config__azure_synapse__pool_name=AZURE_SYNAPSE_SPARK_POOL,\n", + " spark_config__azure_synapse__workspace_dir=AZURE_SYNAPSE_WORKING_DIR,\n", " spark_config__databricks__workspace_instance_url=SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL,\n", " databricks_cluster_id=DATABRICKS_CLUSTER_ID,\n", " )\n", @@ -333,6 +360,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -343,7 +371,7 @@ } }, "source": [ - "# Initialize Feathr Client" + "### Initialize Feathr Client" ] }, { @@ -367,10 +395,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Prepare Datasets\n", + "## 3. Prepare Datasets\n", "\n", "1. Download datasets\n", - "2. Upload to cloud if necessary so that the target cluster can consume as the data source" + "2. Upload to cloud storage if necessary so that the target cluster can consume them as the data sources" ] }, { @@ -455,8 +483,14 @@ } }, "source": [ - "## Explore the raw source data\n", - "We have 4 datasets to work with: one observation dataset (a.k.a. label dataset), two raw datasets to generate features for users, one raw datasets to generate features for product." + "## 3. Define Sharable Features using Feathr API\n", + "\n", + "### Understand raw datasets\n", + "We have three datasets to work with:\n", + "* Observation dataset (a.k.a. labeled dataset)\n", + "* User profile\n", + "* User purchase history\n", + "* Product details" ] }, { @@ -472,7 +506,7 @@ }, "outputs": [], "source": [ - "# Observation dataset(a.k.a. label dataset)\n", + "# Observation dataset\n", "# Observation dataset usually comes with a event_timestamp to denote when the observation happened.\n", "# The label here is product_rating. Our model objective is to predict a user's rating for this product.\n", "pd.read_csv(user_observation_file_path).head()" @@ -533,6 +567,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -543,15 +578,22 @@ } }, "source": [ - "## Defining Features with Feathr\n", - "Let's try to create features from those raw source data.\n", - "In Feathr, a feature is viewed as a function, mapping from entity id or key, and timestamp to a feature value. For more details on feature definition, please refer to the [Feathr Feature Definition Guide](https://github.com/feathr-ai/feathr/blob/main/docs/concepts/feature-definition.md)\n", + "### What's a feature in Feathr\n", + "A feature is an individual measurable property or characteristic of a phenomenon which is sometimes time-sensitive. \n", + "\n", + "In Feathr, a feature is defined by the following characteristics:\n", + "* The typed key (a.k.a. entity id): identifies the subject of feature, e.g. a user id of 123, a product id of SKU234456.\n", + "* The feature name: the unique identifier of the feature, e.g. user_age, total_spending_in_30_days.\n", + "* The feature value: the actual value of that aspect at a particular time, e.g. the feature value of the person's age is 30 at year 2022.\n", + "* The timestamp: this indicates when the event happened. For example, the user purchased certain product on a certain timestamp. This is usually used for point-in-time join.\n", "\n", + "You can feel that this is defined from a feature consumer (a person who wants to use a feature) perspective. It only tells us what a feature is like. In later sections, you can see how a feature consumer can access the features in a very simple way.\n", "\n", - "1. The typed key (a.k.a. entity key) identifies the subject of feature, e.g. a user id, 123.\n", - "2. The feature name is the aspect of the entity that the feature is indicating, e.g. the age of the user.\n", - "3. The feature value is the actual value of that aspect at a particular time, e.g. the value is 30 at year 2022.\n", - "4. The timestamp indicates when the event happened. For example, the user purchased certain product on a certain timestamp. This is usually used for point-in-time join." + "To define how to produce the feature, we need to specify:\n", + "* Feature source: what source data that this feature is based on\n", + "* Transformation: what transformation is used to transform the source data into feature. Transformation can be optional when you just want to take a column out from the source data.\n", + "\n", + "(For more details on feature definition, please refer to the [Feathr Feature Definition Guide](https://feathr-ai.github.io/feathr/concepts/feature-definition.html).)" ] }, { @@ -572,6 +614,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -584,11 +627,11 @@ "source": [ "### Define Sources Section with UDFs\n", "\n", - "#### Define Anchors and Features\n", "A feature is called an anchored feature when the feature is directly extracted from the source data, rather than computed on top of other features. The latter case is called derived feature.\n", "\n", - "#### Feature source\n", - "A feature source is needed for anchored features that describes the raw data in which the feature values are computed from. See the python documentation to get the details on each input column." + "A [feature source](https://feathr.readthedocs.io/en/latest/#feathr.Source) is needed for anchored features that describes the raw data in which the feature values are computed from. See the python documentation to get the details on each input column.\n", + "\n", + "See [the python API documentation](https://feathr.readthedocs.io/en/latest/#feathr.HdfsSource) to get the details of each input fields. " ] }, { @@ -607,8 +650,7 @@ "def feathr_udf_preprocessing(df: DataFrame) -> DataFrame:\n", " from pyspark.sql.functions import col\n", "\n", - " df = df.withColumn(\"tax_rate_decimal\", col(\"tax_rate\") / 100)\n", - " return df\n", + " return df.withColumn(\"tax_rate_decimal\", col(\"tax_rate\") / 100)\n", "\n", "\n", "batch_source = HdfsSource(\n", @@ -722,6 +764,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -732,23 +775,24 @@ } }, "source": [ - "### Window aggregation features\n", + "### Define window aggregation features\n", "\n", - "For window aggregation features, see the supported fields below:\n", + "[Window aggregation](https://en.wikipedia.org/wiki/Window_function_%28SQL%29) helps us to create more powerful features by compressing large amount of information. For example, we can compute *average purchase amount over the last 90 days* from the purchase history to capture user's recent consumption trend.\n", "\n", - "Note that the `agg_func` should be any of these:\n", + "To create window aggregation features, we define `WindowAggTransformation` with following arguments:\n", + "1. `agg_expr`: the field/column you want to aggregate. It can be an ANSI SQL expression, e.g. `cast_float(purchase_amount)` to cast `str` type values to `float`.\n", + "2. `agg_func`: the aggregation function, e.g. `AVG`. See below table for the full list of supported functions.\n", + "3. `window`: the aggregation window size, e.g. `90d` to aggregate over the 90 days.\n", "\n", "| Aggregation Type | Input Type | Description |\n", "| --- | --- | --- |\n", - "|SUM, COUNT, MAX, MIN, AVG\t|Numeric|Applies the the numerical operation on the numeric inputs. |\n", - "|MAX_POOLING, MIN_POOLING, AVG_POOLING\t| Numeric Vector | Applies the max/min/avg operation on a per entry bassis for a given a collection of numbers.|\n", - "|LATEST| Any |Returns the latest not-null values from within the defined time window |\n", - "\n", + "| `SUM`, `COUNT`, `MAX`, `MIN`, `AVG` | Numeric | Applies the the numerical operation on the numeric inputs. |\n", + "| `MAX_POOLING`, `MIN_POOLING`, `AVG_POOLING`\t| Numeric Vector | Applies the max/min/avg operation on a per entry basis for a given a collection of numbers. |\n", + "| `LATEST` | Any | Returns the latest not-null values from within the defined time window. |\n", "\n", "After you have defined features and sources, bring them together to build an anchor:\n", "\n", - "\n", - "Note that if the data source is from the observation data, the `source` section should be `INPUT_CONTEXT` to indicate the source of those defined anchors." + "> Note that if the features comes directly from the observation data, the `source` argument should be `INPUT_CONTEXT` to indicate the source of the anchor is the observation data." ] }, { @@ -788,6 +832,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -799,7 +844,7 @@ }, "source": [ "### Derived Features Section\n", - "Derived features are the features that are computed from other features. They could be computed from anchored features, or other derived features." + "Derived features are the features that are computed from other features. They could be computed from anchored features or other derived features." ] }, { @@ -815,16 +860,19 @@ }, "outputs": [], "source": [ - "feature_user_purchasing_power = DerivedFeature(\n", - " name=\"feature_user_purchasing_power\",\n", - " key=user_id,\n", - " feature_type=FLOAT,\n", - " input_features=[feature_user_gift_card_balance, feature_user_has_valid_credit_card],\n", - " transform=\"feature_user_gift_card_balance + if(boolean(feature_user_has_valid_credit_card), 100, 0)\",\n", - ")" + "derived_features = [\n", + " DerivedFeature(\n", + " name=\"feature_user_purchasing_power\",\n", + " key=user_id,\n", + " feature_type=FLOAT,\n", + " input_features=[feature_user_gift_card_balance, feature_user_has_valid_credit_card],\n", + " transform=\"feature_user_gift_card_balance + if(boolean(feature_user_has_valid_credit_card), 100, 0)\",\n", + " )\n", + "]" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -835,7 +883,9 @@ } }, "source": [ - "And then we need to build those features so that it can be consumed later. Note that we have to build both the \"anchor\" and the \"derived\" features (which is not anchored to a source)." + "### Build features\n", + "\n", + "Lastly, we need to build those features so that it can be consumed later. Note that we have to build both the \"anchor\" and the \"derived\" features which is not anchored to a source." ] }, { @@ -853,11 +903,12 @@ "source": [ "client.build_features(\n", " anchor_list=[user_agg_feature_anchor, user_feature_anchor, product_feature_anchor],\n", - " derived_feature_list=[feature_user_purchasing_power],\n", + " derived_feature_list=derived_features,\n", ")" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -868,14 +919,13 @@ } }, "source": [ - "## Create training data using point-in-time correct feature join\n", + "## 4. Create Training Data using Point-in-Time Correct Feature join\n", "\n", - "A training dataset usually contains entity id columns, multiple feature columns, event timestamp column and label/target column. \n", + "To create a training dataset using Feathr, we need to provide a **feature join settings** to specify what features and how these features should be joined to the observation data. \n", "\n", - "To create a training dataset using Feathr, one needs to provide a feature join configuration file to specify\n", - "what features and how these features should be joined to the observation data. \n", + "Also note that since a `FeatureQuery` accepts features of the same join key, we define two query objects, one for `user_id` key and the other one for `product_id` and pass them together to compute offline features. \n", "\n", - "To learn more on this topic, please refer to [Point-in-time Correctness](https://github.com/feathr-ai/feathr/blob/main/docs/concepts/point-in-time-join.md)" + "To learn more on this topic, please refer to [Point-in-time Correctness document](https://feathr-ai.github.io/feathr/concepts/point-in-time-join.html)." ] }, { @@ -892,19 +942,13 @@ "outputs": [], "source": [ "user_feature_query = FeatureQuery(\n", - " feature_list=[\n", - " \"feature_user_age\",\n", - " \"feature_user_tax_rate\",\n", - " \"feature_user_gift_card_balance\",\n", - " \"feature_user_has_valid_credit_card\",\n", - " \"feature_user_avg_purchase_for_90days\",\n", - " \"feature_user_purchasing_power\",\n", - " ],\n", + " feature_list=[feat.name for feat in features + agg_features + derived_features],\n", " key=user_id,\n", ")\n", "\n", "product_feature_query = FeatureQuery(\n", - " feature_list=[\"feature_product_quantity\", \"feature_product_price\"], key=product_id\n", + " feature_list=[feat.name for feat in product_features],\n", + " key=product_id,\n", ")\n", "\n", "settings = ObservationSettings(\n", @@ -921,6 +965,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -931,8 +976,6 @@ } }, "source": [ - "## Download the training dataset and show the result\n", - "\n", "Let's use the helper function `get_result_df` to download the result and view it:" ] }, @@ -954,6 +997,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -964,8 +1008,8 @@ } }, "source": [ - "## Train a machine learning model\n", - "After getting all the features, let's train a machine learning model with the converted feature by Feathr:" + "### Train a machine learning model\n", + "After getting all the features, let's train a machine learning model with the converted feature by Feathr. Here, we use **EBM (Explainable Boosting Machine)** regressor from [InterpretML](https://github.com/interpretml/interpret) package to visualize the modeling results." ] }, { @@ -981,33 +1025,51 @@ }, "outputs": [], "source": [ - "from sklearn.ensemble import GradientBoostingRegressor\n", + "from interpret import show\n", + "from interpret.glassbox import ExplainableBoostingRegressor\n", "from sklearn.metrics import mean_squared_error\n", "from sklearn.model_selection import train_test_split\n", "\n", "\n", + "# Fill None values with 0\n", "final_df = (\n", " res_df\n", " .drop([\"event_timestamp\"], axis=1, errors=\"ignore\")\n", " .fillna(0)\n", ")\n", "\n", + "# Split data into train and test\n", "X_train, X_test, y_train, y_test = train_test_split(\n", " final_df.drop([\"product_rating\"], axis=1),\n", " final_df[\"product_rating\"].astype(\"float64\"),\n", " test_size=0.2,\n", " random_state=42,\n", ")\n", - "model = GradientBoostingRegressor()\n", - "model.fit(X_train, y_train)\n", "\n", - "y_pred = model.predict(X_test)\n", + "ebm = ExplainableBoostingRegressor()\n", + "ebm.fit(X_train, y_train)\n", + "\n", + "# Note, currently InterpretML's visualization dashboard doesn't work w/ VSCODE notebook viewer\n", + "# https://github.com/interpretml/interpret/issues/317\n", + "ebm_global = ebm.explain_global()\n", + "show(ebm_global)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Predict and evaluate\n", + "y_pred = ebm.predict(X_test)\n", "rmse = sqrt(mean_squared_error(y_test.values.flatten(), y_pred))\n", "\n", "print(f\"Root mean squared error: {rmse}\")" ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -1018,7 +1080,7 @@ } }, "source": [ - "## Materialize feature value into offline/online storage\n", + "## 5. Feature Materialization\n", "\n", "While Feathr can compute the feature value from the feature definition on-the-fly at request time, it can also pre-compute\n", "and materialize the feature value to offline and/or online storage. \n", @@ -1066,23 +1128,6 @@ "We can then get the features from the online store (Redis):" ] }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "ed5da7df-8095-403e-91a6-c5d2104eaf68", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Fetching feature value for online inference\n", - "\n", - "For features that are already materialized by the previous step, their latest value can be queried via the client's\n", - "`get_online_features` or `multi_get_online_features` API." - ] - }, { "cell_type": "code", "execution_count": null, @@ -1120,6 +1165,7 @@ ] }, { + "attachments": {}, "cell_type": "markdown", "metadata": { "application/vnd.databricks.v1+cell": { @@ -1130,8 +1176,6 @@ } }, "source": [ - "## Materialize product features\n", - "\n", "We can also materialize product features into a separate table." ] }, @@ -1195,9 +1239,8 @@ } }, "source": [ - "## Registering and Fetching features\n", - "\n", - "We can also register the features with an Apache Atlas compatible service, such as Azure Purview, and share the registered features across teams:" + "## 6. Feature Registration\n", + "Lastly, we can also register the features and share them across teams:" ] }, { @@ -1216,18 +1259,62 @@ "if REGISTER_FEATURES:\n", " try:\n", " client.register_features()\n", - " except KeyError:\n", - " # TODO temporarily go around the \"Already exists\" error\n", - " pass\n", + " except Exception as e:\n", + " print(e)\n", " print(client.list_registered_features(project_name=PROJECT_NAME))" ] }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cleanup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Cleaning up the output files. CAUTION: this maybe dangerous if you \"reused\" the project name.\n", + "import shutil\n", + "shutil.rmtree(WORKING_DIR, ignore_errors=False)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Scrap Variables for Unit-Test" + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "if SCRAP_RESULTS:\n", + " # Record results for test pipelines\n", + " import scrapbook as sb\n", + " sb.glue(\n", + " \"user_features\",\n", + " client.get_online_features(\n", + " \"user_features\", \"2\", [\"feature_user_age\", \"feature_user_gift_card_balance\"]\n", + " ),\n", + " )\n", + " sb.glue(\n", + " \"product_features\",\n", + " client.get_online_features(\n", + " \"product_features\", \"2\", [\"feature_product_price\"]\n", + " ),\n", + " )\n", + " \n", + " sb.glue(\"rmse\", rmse)" + ] } ], "metadata": { @@ -1256,7 +1343,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8 (main, Nov 24 2022, 14:13:03) [GCC 11.2.0]" + "version": "3.10.8" }, "vscode": { "interpreter": { diff --git a/feathr_project/test/samples/test_notebooks.py b/feathr_project/test/samples/test_notebooks.py index c5e1f962b..d14516bea 100644 --- a/feathr_project/test/samples/test_notebooks.py +++ b/feathr_project/test/samples/test_notebooks.py @@ -21,6 +21,7 @@ "nyc_taxi_demo": str(SAMPLES_DIR.joinpath("nyc_taxi_demo.ipynb")), "feature_embedding": str(SAMPLES_DIR.joinpath("feature_embedding.ipynb")), "fraud_detection_demo": str(SAMPLES_DIR.joinpath("fraud_detection_demo.ipynb")), + "product_recommendation_demo_advanced": str(SAMPLES_DIR.joinpath("product_recommendation_demo_advanced.ipynb")), } @@ -99,3 +100,32 @@ def test__fraud_detection_demo(config_path, tmp_path): outputs = nb.scraps assert outputs["materialized_feature_values"].data == pytest.approx([False, 0, 9, 239.0, 1, 1, 239.0, 33816.0], abs=1.) + + +@pytest.mark.notebooks +def test__product_recommendation_demo_advanced(config_path, tmp_path): + notebook_name = "product_recommendation_demo_advanced" + + output_notebook_path = str(tmp_path.joinpath(f"{notebook_name}.ipynb")) + + print(f"Running {notebook_name} notebook as {output_notebook_path}") + + pm.execute_notebook( + input_path=NOTEBOOK_PATHS[notebook_name], + output_path=output_notebook_path, + # kernel_name="python3", + parameters=dict( + FEATHR_CONFIG_PATH=config_path, + USE_CLI_AUTH=False, + REGISTER_FEATURES=False, + SCRAP_RESULTS=True, + ), + ) + + # Read results from the Scrapbook and assert expected values + nb = sb.read_notebook(output_notebook_path) + outputs = nb.scraps + + assert outputs["user_features"].data == pytest.approx([17, 300.0], abs=0.1) + assert outputs["product_features"].data == pytest.approx([17.0], abs=0.1) + assert outputs["rmse"].data == pytest.approx(0.49343, abs=2.0) From c4733745ea15d7af106ce557f977a5ac9f7e3097 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Tue, 27 Dec 2022 15:57:22 -0800 Subject: [PATCH 14/22] Fix numpy.bool deprecation error Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- feathr_project/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feathr_project/setup.py b/feathr_project/setup.py index 660343f07..5b8a6b4d9 100644 --- a/feathr_project/setup.py +++ b/feathr_project/setup.py @@ -64,7 +64,7 @@ "click<=8.1.3", "py4j<=0.10.9.7", "loguru<=0.6.0", - "pandas<=1.5.0", + "pandas>=1.5.0", "redis<=4.4.0", "requests<=2.28.1", "tqdm<=4.64.1", From 2044323d863c70155d2d4e437bfb6b0c35f64bb0 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Wed, 28 Dec 2022 12:00:13 -0800 Subject: [PATCH 15/22] Change databricks cluster node size from Dv2 to DSv2 Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- feathr_project/feathr/utils/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feathr_project/feathr/utils/config.py b/feathr_project/feathr/utils/config.py index 7f9582cf8..d8ce248bc 100644 --- a/feathr_project/feathr/utils/config.py +++ b/feathr_project/feathr/utils/config.py @@ -33,7 +33,7 @@ # New databricks job cluster config DEFAULT_DATABRICKS_CLUSTER_CONFIG = { "spark_version": "11.2.x-scala2.12", - "node_type_id": "Standard_D3_v2", # Change this if necessary + "node_type_id": "Standard_DS3_v2", # Change this if necessary "num_workers": 1, "spark_conf": { "FEATHR_FILL_IN": "FEATHR_FILL_IN", From 7b91b3e064f964ff311320641381f5121e292c96 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Wed, 28 Dec 2022 14:26:08 -0800 Subject: [PATCH 16/22] Use Dv4 for databricks notebook test due to the limit of Dv2 quota at US East 2 Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/feature_embedding.ipynb | 9 ++++++++- feathr_project/test/samples/test_notebooks.py | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/samples/feature_embedding.ipynb b/docs/samples/feature_embedding.ipynb index 048562dbd..af34475ec 100755 --- a/docs/samples/feature_embedding.ipynb +++ b/docs/samples/feature_embedding.ipynb @@ -44,6 +44,7 @@ }, "outputs": [], "source": [ + "from copy import deepcopy\n", "import json\n", "import os\n", "\n", @@ -121,6 +122,9 @@ " DATABRICKS_WORKSPACE_TOKEN_VALUE = os.environ.get(\"DATABRICKS_WORKSPACE_TOKEN_VALUE\")\n", " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = os.environ.get(\"SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL\")\n", "\n", + "# TODO Change the value if necessary\n", + "DATABRICKS_NODE_SIZE = \"Standard_DS3_v2\"\n", + "\n", "# We'll need an authentication credential to access Azure resources and register features \n", "USE_CLI_AUTH = False # Set True to use interactive authentication\n", "\n", @@ -286,6 +290,9 @@ }, "outputs": [], "source": [ + "databricks_cluster_config = deepcopy(DEFAULT_DATABRICKS_CLUSTER_CONFIG)\n", + "databricks_cluster_config[\"node_type_id\"] = DATABRICKS_NODE_SIZE\n", + "\n", "databricks_config = {\n", " \"run_name\": \"FEATHR_FILL_IN\",\n", " \"libraries\": [\n", @@ -297,7 +304,7 @@ " \"main_class_name\": \"FEATHR_FILL_IN\",\n", " \"parameters\": [\"FEATHR_FILL_IN\"],\n", " },\n", - " \"new_cluster\": DEFAULT_DATABRICKS_CLUSTER_CONFIG,\n", + " \"new_cluster\": databricks_cluster_config,\n", "}\n", "\n", "config_path = generate_config(\n", diff --git a/feathr_project/test/samples/test_notebooks.py b/feathr_project/test/samples/test_notebooks.py index a1c62b17c..d78c95ac3 100644 --- a/feathr_project/test/samples/test_notebooks.py +++ b/feathr_project/test/samples/test_notebooks.py @@ -67,6 +67,7 @@ def test__feature_embedding(tmp_path): output_path=output_notebook_path, # kernel_name="python3", parameters=dict( + DATABRICKS_NODE_SIZE="Standard_D4s_v4", # use Dv4 due to the quota limit of Dv2 at US East 2 region. USE_CLI_AUTH=False, REGISTER_FEATURES=False, CLEAN_UP=True, From bad967de9ad42c7cc6f2a9215bd7c493ee1e961e Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Wed, 28 Dec 2022 15:13:23 -0800 Subject: [PATCH 17/22] Fix to use the supported vm size Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- feathr_project/test/samples/test_notebooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feathr_project/test/samples/test_notebooks.py b/feathr_project/test/samples/test_notebooks.py index d78c95ac3..f462082cb 100644 --- a/feathr_project/test/samples/test_notebooks.py +++ b/feathr_project/test/samples/test_notebooks.py @@ -67,7 +67,7 @@ def test__feature_embedding(tmp_path): output_path=output_notebook_path, # kernel_name="python3", parameters=dict( - DATABRICKS_NODE_SIZE="Standard_D4s_v4", # use Dv4 due to the quota limit of Dv2 at US East 2 region. + DATABRICKS_NODE_SIZE="Standard_D4s_v5", # use Dv5 due to the quota limit of Dv2 at US East 2 region. USE_CLI_AUTH=False, REGISTER_FEATURES=False, CLEAN_UP=True, From 73f60de83295d0cc9db4bff4e0f0d61bf05a5336 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Wed, 28 Dec 2022 23:27:53 -0800 Subject: [PATCH 18/22] pin numpy to resolve conflict with pyspark Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- feathr_project/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/feathr_project/setup.py b/feathr_project/setup.py index 5b8a6b4d9..c47e02508 100644 --- a/feathr_project/setup.py +++ b/feathr_project/setup.py @@ -65,6 +65,7 @@ "py4j<=0.10.9.7", "loguru<=0.6.0", "pandas>=1.5.0", + "numpy<=1.20.3", # pin numpy due to pyspark's deprecated np.bool access "redis<=4.4.0", "requests<=2.28.1", "tqdm<=4.64.1", From c1f791adc9c918396d97c5222aae73b3e158361c Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Tue, 3 Jan 2023 18:30:49 +0000 Subject: [PATCH 19/22] Add document intelligence sample notebook Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- ...gnitive-search-enrichment-architecture.png | Bin 0 -> 94900 bytes ...e_with_azure_cognitive_search_skills.ipynb | 1249 +++++++++++++++++ feathr_project/feathr/client.py | 4 +- 3 files changed, 1251 insertions(+), 2 deletions(-) create mode 100755 docs/images/cognitive-search-enrichment-architecture.png create mode 100644 docs/samples/document_intelligence_with_azure_cognitive_search_skills.ipynb diff --git a/docs/images/cognitive-search-enrichment-architecture.png b/docs/images/cognitive-search-enrichment-architecture.png new file mode 100755 index 0000000000000000000000000000000000000000..4b405867c24511325ded2276cf8bbcfaee322518 GIT binary patch literal 94900 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%0Dpmg)yWa<)i$V8PonC#v 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⪚PNwcFykShMuJR*eO&X^OOZ6SfE|6@C7 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 U$1cV5N2rg|TcF%8nNNZL1(i`2-2eap literal 0 HcmV?d00001 diff --git a/docs/samples/document_intelligence_with_azure_cognitive_search_skills.ipynb b/docs/samples/document_intelligence_with_azure_cognitive_search_skills.ipynb new file mode 100644 index 000000000..55d55d97c --- /dev/null +++ b/docs/samples/document_intelligence_with_azure_cognitive_search_skills.ipynb @@ -0,0 +1,1249 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "bbff7856", + "metadata": {}, + "source": [ + "# Document Intelligence with Azure Cognitive Search Skillset\n", + "\n", + "AI enrichment is the application of machine learning models over content that isn't full text searchable in its raw form. Through enrichment, analysis and inference are used to create searchable content and structure where none previously existed.\n", + "In this notebook, we use an [AI enrichment pipeline with Azure Cognitive Search](https://learn.microsoft.com/en-us/azure/search/cognitive-search-concept-intro) to extract information and create new content from various formats of document files.\n", + "After we extract the enriched documents, we define a Feathr feature, materialize it, and utilize it from NLP (Natural Language Processing) scenarios such as [question-answering](https://huggingface.co/docs/transformers/tasks/question_answering) and [summarization](https://huggingface.co/tasks/summarization).\n", + "\n", + "The overall workflow is:\n", + "1. Deploy Azure Cognitive Search and Feathr resources.\n", + "2. Prepare mixed media sample documents.\n", + "3. Extract texts by using Azure Cognitive Search Skillset and store the results.\n", + "4. Define a Feathr feature with the enriched documents, register the feature, and materialize it.\n", + "5. Use the feature for NLP scenarios." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7da244ca", + "metadata": {}, + "source": [ + "## 1. Deployment\n", + "\n", + "### Deploy Azure Cognitive Search service\n", + "Please follow [this link](https://learn.microsoft.com/en-us/azure/search/search-create-service-portal) to create the search service from the Azure portal.\n", + "\n", + "### Deploy Feathr resources\n", + "Please follow [this link](https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-arm.html) to deploy necessary resources via ARM template and grant permissions to access them." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d2b26860", + "metadata": {}, + "source": [ + "## 2. Prepare Dataset\n", + "In this notebook, we use mixed media documents from an [Azure sample repository](https://github.com/Azure-Samples/azure-search-knowledge-mining).\n", + "\n", + "1. Download document files from the *[https://github.com/Azure-Samples/azure-search-knowledge-mining/tree/main/sample_documents](https://github.com/Azure-Samples/azure-search-knowledge-mining/tree/main/sample_documents)*\n", + "2. Create a container called `cogsearch` at the Storage Account under the Cognitive Search resource group you deployed\n", + "3. Upload the document files to the container " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2817d55c", + "metadata": {}, + "source": [ + "## 3. AI Enrichment by using Azure Cognitive Search Skillset\n", + "\n", + "Before move on to the following steps, let's go over some of the important concepts in [Azure Cognitive Search](https://learn.microsoft.com/en-us/azure/search/search-what-is-azure-search):\n", + "* **Azure Cognitive Search** is a cloud search service that gives developers infrastructure, APIs, and tools for building a rich search experience over private, heterogeneous content in web, mobile, and enterprise applications.\n", + "* **Indexing** is an intake process that loads content into your search service and makes it searchable. AI enrichment through cognitive skills is an extension of indexing. If your content needs image or language analysis before it can be indexed, AI enrichment can extract text embedded in application files, translate text, and also infer text and structure from non-text files by analyzing the content.\n", + "* **Skillset** is a reusable resource in Azure Cognitive Search that's attached to an indexer. It contains one or more skills that call built-in AI or external custom processing over documents retrieved from an external data source.\n", + "* **Knowledge store** is a data sink created by a Cognitive Search enrichment pipeline that stores AI-enriched content in tables and blob containers in Azure Storage for independent analysis or downstream processing in non-search scenarios like knowledge mining.\n", + "\n", + "In this notebook, we use two built-in skills:\n", + "* Translation and language detection\n", + "* Optical Character Recognition (OCR) that recognizes printed and handwritten text in binary files\n", + "\n", + "For more details about the built-in skillset, see [here](https://learn.microsoft.com/en-us/azure/search/cognitive-search-predefined-skills).\n", + "\n", + "![Cognitive Search](../images/cognitive-search-enrichment-architecture.png)\n", + "\n", + "\n", + "### 3.1 Set parameters for connecting to the resources" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0beae6e1", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "from pprint import pprint\n", + "import requests\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21f08bb0", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO fill the values:\n", + "SERVICE_NAME = None # Search Service name and API key\n", + "API_KEY = None\n", + "STORAGE_NAME = None # Storage account for Azure Cognitive Search datasource and knowledge store\n", + "STORAGE_KEY = None\n", + "\n", + "CONTAINER_NAME = \"cogsearch\"\n", + "API_VERSION = \"2021-04-30-Preview\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb1d7729", + "metadata": {}, + "outputs": [], + "source": [ + "# Storage account connection string\n", + "storage_connection_str = f\"DefaultEndpointsProtocol=https;AccountName={STORAGE_NAME};AccountKey={STORAGE_KEY};EndpointSuffix=core.windows.net\"" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ac4aa5d1", + "metadata": {}, + "source": [ + "To verify the access to the storage account with `STORAGE_NAME` and `STORAGE_KEY` we set from the previous cell, let's list the document blob names in the `cogsearch` container. Please make sure you created the container and uploaded the documents from the section **2. Prepare Datasets**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c16c250b", + "metadata": {}, + "outputs": [], + "source": [ + "from azure.storage.blob import ContainerClient\n", + "\n", + "container_client = ContainerClient.from_connection_string(\n", + " storage_connection_str,\n", + " container_name=CONTAINER_NAME,\n", + ")\n", + "\n", + "# Get name of the blobs\n", + "output = [blob.name for blob in container_client.list_blobs()]\n", + "output" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "782f89cd", + "metadata": {}, + "source": [ + "### 3.2 Create DataSource, Index, and Indexer\n", + "\n", + "In Azure Cognitive Search, AI enrichment processing occurs during indexing (or data ingestion). The pipeline consists of:\n", + "* Data source\n", + "* Skill set\n", + "* Index, and\n", + "* Indexer.\n", + "\n", + "In this notebook, we use Search REST APIs to create them. First, let's define some helper functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4785b56a", + "metadata": {}, + "outputs": [], + "source": [ + "# Cognitive search endpoint\n", + "endpoint = f\"https://{SERVICE_NAME}.search.windows.net\"\n", + "\n", + "# Cognitive search REST API\n", + "headers = {\n", + " \"Content-Type\": \"application/json\",\n", + " \"api-key\": API_KEY,\n", + "}\n", + "\n", + "# Cognitive search resource names\n", + "datasource_name = f\"{CONTAINER_NAME}-ds\"\n", + "skillset_name = f\"{CONTAINER_NAME}-ss\"\n", + "index_name = f\"{CONTAINER_NAME}-idx\"\n", + "indexer_name = f\"{CONTAINER_NAME}-idxr\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72be1e7c", + "metadata": {}, + "outputs": [], + "source": [ + "def construct_url(\n", + " endpoint: str,\n", + " resource_type: str,\n", + " resource_name: str = None,\n", + " action: str = None,\n", + " api_version: str = API_VERSION,\n", + "):\n", + " \"\"\"Construct url for REST API calls.\"\"\"\n", + " components = [endpoint, resource_type]\n", + " \n", + " if resource_name:\n", + " components.append(resource_name)\n", + " if action:\n", + " components.append(action)\n", + " \n", + " return \"/\".join(components) + f\"?api-version={api_version}\"\n", + "\n", + "\n", + "def create_or_update_resource(resource_type: str, resource_name: str, resource_def: dict):\n", + " \"\"\"Create or update Azure Cognitive Search resources.\"\"\"\n", + " r = requests.put(\n", + " construct_url(endpoint, resource_type, resource_name, None, API_VERSION),\n", + " data=json.dumps(resource_def),\n", + " headers=headers,\n", + " )\n", + "\n", + " # The request should return a status code of 201 confirming success.\n", + " if r.status_code == 201:\n", + " print(f\"Successfully created {resource_type} {resource_name}\")\n", + " elif r.status_code == 204:\n", + " print(f\"Successfully updated {resource_type} {resource_name}\")\n", + " else:\n", + " print(r.json()[\"error\"][\"message\"])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2aebc42", + "metadata": {}, + "outputs": [], + "source": [ + "# Clean-up previously created cognitive search resources if already exists\n", + "for resource, resource_name in {\n", + " \"datasources\": datasource_name,\n", + " \"skillsets\": skillset_name,\n", + " \"indexes\": index_name,\n", + " \"indexers\": indexer_name,\n", + "}.items():\n", + " r = requests.delete(\n", + " construct_url(endpoint, resource, resource_name),\n", + " headers=headers,\n", + " )\n", + " if r.status_code == 204:\n", + " print(f\"{resource} {resource_name} is successfully deleted.\")\n", + " else:\n", + " print(r.json()[\"error\"][\"message\"])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "5d5644ef", + "metadata": {}, + "source": [ + "#### Create a DataSource" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dabe6336", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a data source\n", + "datasource_def = {\n", + " \"name\": datasource_name,\n", + " \"description\": \"Feathr with Cognitive Search example\",\n", + " \"type\": \"azureblob\",\n", + " \"credentials\": {\n", + " \"connectionString\": storage_connection_str,\n", + " },\n", + " \"container\": {\n", + " \"name\": CONTAINER_NAME,\n", + " },\n", + "}\n", + "\n", + "create_or_update_resource(\"datasources\", datasource_name, datasource_def)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1b4032b6", + "metadata": {}, + "source": [ + "#### Create a Skillset\n", + "\n", + "Each skill executes on the content of the document. During processing, Azure Cognitive Search cracks each document to read content from different file formats. Found text originating in the source file is placed into a generated `content` field, one for each document.\n", + "\n", + "For more details, see [Cognitive Search predefined skills](\n", + "https://learn.microsoft.com/en-us/azure/search/cognitive-search-predefined-skills)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b5c551d2", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a skillset\n", + "skillset_def = {\n", + " \"name\": skillset_name,\n", + " \"description\": \"Apply OCR, \",\n", + " \"skills\": [\n", + " {\n", + " # Recognizes text and numbers in image files.\n", + " \"@odata.type\": \"#Microsoft.Skills.Vision.OcrSkill\",\n", + " \"context\": \"/document/normalized_images/*\",\n", + " \"defaultLanguageCode\": \"en\",\n", + " \"detectOrientation\": True,\n", + " \"inputs\": [\n", + " {\n", + " \"name\": \"image\",\n", + " \"source\": \"/document/normalized_images/*\"\n", + " }\n", + " ],\n", + " \"outputs\": [\n", + " {\n", + " \"name\": \"text\"\n", + " }\n", + " ]\n", + " },\n", + " {\n", + " # Images and text are separated during the document cracking phase. The merge skill recombines them.\n", + " \"@odata.type\": \"#Microsoft.Skills.Text.MergeSkill\",\n", + " \"context\": \"/document\",\n", + " \"insertPreTag\": \" \",\n", + " \"insertPostTag\": \" \",\n", + " \"inputs\": [\n", + " {\n", + " \"name\":\"text\", \n", + " \"source\": \"/document/content\"\n", + " },\n", + " {\n", + " \"name\": \"itemsToInsert\", \n", + " \"source\": \"/document/normalized_images/*/text\"\n", + " },\n", + " {\n", + " \"name\":\"offsets\", \n", + " \"source\": \"/document/normalized_images/*/contentOffset\" \n", + " }\n", + " ],\n", + " \"outputs\": [\n", + " {\n", + " \"name\": \"mergedText\", \n", + " \"targetName\" : \"merged_text\"\n", + " }\n", + " ]\n", + " },\n", + " {\n", + " # Translates different language text to English.\n", + " \"@odata.type\": \"#Microsoft.Skills.Text.TranslationSkill\",\n", + " \"defaultToLanguageCode\": \"en\",\n", + " \"suggestedFrom\": \"es\",\n", + " \"context\": \"/document\",\n", + " \"inputs\": [\n", + " {\n", + " \"name\": \"text\",\n", + " \"source\": \"/document/merged_text\"\n", + " }\n", + " ],\n", + " \"outputs\": [\n", + " {\n", + " \"name\": \"translatedText\",\n", + " \"targetName\": \"translated_text\"\n", + " },\n", + " {\n", + " \"name\": \"translatedFromLanguageCode\",\n", + " \"targetName\": \"translated_from_language_code\"\n", + " },\n", + " {\n", + " \"name\": \"translatedToLanguageCode\",\n", + " \"targetName\": \"translated_to_language_code\"\n", + " }\n", + " ]\n", + " },\n", + " # Shaper skill to determine the schema and contents of the projection to Knowledge store.\n", + " {\n", + " \"@odata.type\": \"#Microsoft.Skills.Util.ShaperSkill\",\n", + " \"context\": \"/document\",\n", + " \"inputs\": [\n", + " {\n", + " # metadata_storage_name is the document file (blob) name\n", + " \"name\": \"metadata_storage_name\",\n", + " \"source\": \"/document/metadata_storage_name\"\n", + " },\n", + " {\n", + " \"name\": \"text\",\n", + " \"source\": \"/document/translated_text\"\n", + " }\n", + " ],\n", + " \"outputs\": [\n", + " {\n", + " \"name\": \"output\",\n", + " \"targetName\": \"shaper_output\"\n", + " },\n", + " ],\n", + " },\n", + " ],\n", + " # Free skillset execution quota is 20 documents. To process more, set Azure Cognitive Service here.\n", + " \"cognitiveServices\": None,\n", + " # A knowledge store makes enriched content available in Azure Storage for downstream apps and workloads.\n", + " \"knowledgeStore\": {\n", + " \"storageConnectionString\": storage_connection_str,\n", + " \"projections\": [\n", + " {\n", + " # Store enrichment results into blobs:\n", + " \"objects\": [{\"storageContainer\": f\"{CONTAINER_NAME}output\", \"source\": \"/document/shaper_output\"}],\n", + " },\n", + " ],\n", + " },\n", + "}\n", + "\n", + "create_or_update_resource(\"skillsets\", skillset_name, skillset_def)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "12b79360", + "metadata": {}, + "source": [ + "#### Create an Index\n", + "\n", + "Provide the schema of the search index. A fields collection requires one field to be designated as the key. For blob content, this field is often the `metadata_storage_path` that uniquely identifies each blob in the container.\n", + "\n", + "In this schema, the \"text\" field receives OCR output, \"raw_content\" receives merged output, and \"content\" receives translation output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be5585b0", + "metadata": {}, + "outputs": [], + "source": [ + "# Create an index\n", + "index_def = {\n", + " \"name\": index_name,\n", + " \"fields\": [\n", + " {\n", + " \"name\": \"text\",\n", + " \"type\": \"Collection(Edm.String)\",\n", + " \"searchable\": True,\n", + " \"sortable\": False,\n", + " \"filterable\": True,\n", + " \"facetable\": False\n", + " },\n", + " {\n", + " \"name\": \"content\",\n", + " \"type\": \"Edm.String\",\n", + " \"searchable\": True,\n", + " \"sortable\": False,\n", + " \"filterable\": False,\n", + " \"facetable\": False\n", + " },\n", + " {\n", + " \"name\": \"raw_content\",\n", + " \"type\": \"Edm.String\",\n", + " \"searchable\": False,\n", + " \"sortable\": False,\n", + " \"filterable\": False,\n", + " \"facetable\": False\n", + " },\n", + " {\n", + " \"name\": \"metadata_storage_path\",\n", + " \"type\": \"Edm.String\",\n", + " \"key\": True,\n", + " \"searchable\": True,\n", + " \"sortable\": False,\n", + " \"filterable\": False,\n", + " \"facetable\": False\n", + " },\n", + " {\n", + " \"name\": \"metadata_storage_name\",\n", + " \"type\": \"Edm.String\",\n", + " \"searchable\": True,\n", + " \"sortable\": False,\n", + " \"filterable\": True,\n", + " \"facetable\": False\n", + " }\n", + " ]\n", + "}\n", + "\n", + "create_or_update_resource(\"indexes\", index_name, index_def)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e21dc9c7", + "metadata": {}, + "source": [ + "#### Create and Run an Indexer\n", + "\n", + "Creating an indexer invokes the pipeline." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1dc507f3", + "metadata": {}, + "outputs": [], + "source": [ + "indexer_def = {\n", + " \"name\": indexer_name,\n", + " \"dataSourceName\": datasource_name,\n", + " \"targetIndexName\": index_name,\n", + " \"skillsetName\": skillset_name,\n", + " \"cache\": {\n", + " \"enableReprocessing\": True,\n", + " \"storageConnectionString\": storage_connection_str,\n", + " },\n", + " # fieldMappings are processed before the skillset, sending content from the data source to target fields in an index.\n", + " \"fieldMappings\": [\n", + " {\n", + " \"sourceFieldName\": \"metadata_storage_path\",\n", + " \"targetFieldName\": \"metadata_storage_path\",\n", + " \"mappingFunction\": {\"name\": \"base64Encode\"}\n", + " },\n", + " {\n", + " \"sourceFieldName\": \"metadata_storage_name\",\n", + " \"targetFieldName\": \"metadata_storage_name\"\n", + " }\n", + " ],\n", + " # outputFieldMappings are for fields created by skills, after skillset execution.\n", + " # The references to sourceFieldName in outputFieldMappings don't exist until document cracking or enrichment creates them.\n", + " # The targetFieldName is a field in an index, defined in the index schema.\n", + " \"outputFieldMappings\": [\n", + " {\n", + " \"sourceFieldName\": \"/document/merged_text\",\n", + " \"targetFieldName\": \"raw_content\"\n", + " },\n", + " {\n", + " \"sourceFieldName\": \"/document/translated_text\",\n", + " \"targetFieldName\": \"content\"\n", + " },\n", + " {\n", + " \"sourceFieldName\": \"/document/normalized_images/*/text\",\n", + " \"targetFieldName\": \"text\"\n", + " }\n", + " ],\n", + " \"parameters\":\n", + " {\n", + " \"batchSize\": 1,\n", + " \"maxFailedItems\": -1, # -1 to ignore errors during data import\n", + " \"maxFailedItemsPerBatch\": -1,\n", + " \"configuration\": \n", + " {\n", + " \"dataToExtract\": \"contentAndMetadata\", # automatically extract the content from different file formats as well as metadata related to each file\n", + " \"imageAction\": \"generateNormalizedImages\" # combined with the OCR Skill and Text Merge Skill, tells the indexer to extract text from the images\n", + " }\n", + " } \n", + "}\n", + "\n", + "create_or_update_resource(\"indexers\", indexer_name, indexer_def)" + ] + }, + { + "cell_type": "markdown", + "id": "7b6b1435", + "metadata": {}, + "source": [ + "Previous cell will create the indexer and run it. Now, we wait until the indexer run and check the result after done." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58b27a5e", + "metadata": {}, + "outputs": [], + "source": [ + "while True:\n", + " r = requests.get(\n", + " construct_url(endpoint, \"indexers\", indexer_name, action=\"status\"),\n", + " headers=headers,\n", + " )\n", + " \n", + " try:\n", + " if r.json()[\"lastResult\"][\"endTime\"] is not None:\n", + " break\n", + " except:\n", + " pass\n", + "\n", + " time.sleep(3)\n", + "\n", + "pprint(json.dumps(r.json(), indent=1))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "8b30ead7", + "metadata": {}, + "source": [ + "#### Test Search\n", + "\n", + "With REST API call, we can verify the result of cognitive search skills. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14c46d62", + "metadata": {}, + "outputs": [], + "source": [ + "r = requests.post(\n", + " construct_url(\n", + " endpoint=endpoint,\n", + " resource_type=\"indexes\",\n", + " resource_name=index_name + \"/docs\",\n", + " action=\"search\",\n", + " ),\n", + " data=json.dumps({\n", + " \"search\": \"*\",\n", + " \"filter\": \"metadata_storage_name eq 'Cognitive Services and Bots (spanish).pdf'\", #'Mesh_for_Microsoft_Teams.docx'\",\n", + " \"select\": \"raw_content, content\",\n", + " }),\n", + " headers=headers,\n", + ")\n", + "\n", + "result = r.json()['value'][0]\n", + "print(\n", + " f\"[Content]{result['content'][:100]}\",\n", + " f\"[Raw content]{result['raw_content'][:100]}\",\n", + " sep=\"\\n=========\\n\",\n", + ")\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "677fba41", + "metadata": {}, + "source": [ + "## 4. Feathr Feature Store" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3879532d", + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "import os\n", + "from pathlib import Path\n", + "import shutil\n", + "\n", + "from pyspark.sql import DataFrame\n", + "\n", + "import feathr\n", + "from feathr import (\n", + " FeathrClient,\n", + " # Feature data types\n", + " STRING, ValueType,\n", + " # Feature data sources\n", + " HdfsSource,\n", + " # Feature key\n", + " TypedKey,\n", + " # Feature types and anchor\n", + " Feature, FeatureAnchor,\n", + " # Materialization\n", + " MaterializationSettings, RedisSink,\n", + " # Offline feature computation\n", + " FeatureQuery, ObservationSettings,\n", + ")\n", + "from feathr.spark_provider.feathr_configurations import SparkExecutionConfiguration\n", + "from feathr.utils.config import generate_config\n", + "from feathr.utils.job_utils import get_result_df\n", + "from feathr.utils.platform import is_databricks\n", + "\n", + "print(f\"Feathr version: {feathr.__version__}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85d60f64", + "metadata": {}, + "outputs": [], + "source": [ + "RESOURCE_PREFIX = None # TODO fill the value used to deploy the Feathr resources via ARM template\n", + "PROJECT_NAME = \"cogsearch\"\n", + "\n", + "# Currently support: 'azure_synapse', 'databricks', and 'local' \n", + "SPARK_CLUSTER = \"local\"\n", + "\n", + "# TODO fill values to use databricks cluster:\n", + "DATABRICKS_CLUSTER_ID = None # Set Databricks cluster id to use an existing cluster\n", + "if is_databricks():\n", + " # If this notebook is running on Databricks, its context can be used to retrieve token and instance URL\n", + " ctx = dbutils.notebook.entry_point.getDbutils().notebook().getContext()\n", + " DATABRICKS_WORKSPACE_TOKEN_VALUE = ctx.apiToken().get()\n", + " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = f\"https://{ctx.tags().get('browserHostName').get()}\"\n", + "else:\n", + " DATABRICKS_WORKSPACE_TOKEN_VALUE = None # Set Databricks workspace token to use databricks\n", + " SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL = None # Set Databricks workspace url to use databricks\n", + "\n", + "# TODO fill values to use Azure Synapse cluster:\n", + "AZURE_SYNAPSE_SPARK_POOL = None # Set Azure Synapse Spark pool name\n", + "AZURE_SYNAPSE_URL = None # Set Azure Synapse workspace url to use Azure Synapse\n", + "ADLS_KEY = None # Set Azure Data Lake Storage key to use Azure Synapse\n", + "\n", + "# If set True, use an interactive browser authentication to get the redis password.\n", + "USE_CLI_AUTH = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2765a239", + "metadata": {}, + "outputs": [], + "source": [ + "# Use dbfs if the notebook is running on Databricks\n", + "if is_databricks():\n", + " WORKING_DIR = f\"/dbfs/{PROJECT_NAME}\"\n", + "else:\n", + " WORKING_DIR = PROJECT_NAME" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f10d4f66", + "metadata": {}, + "outputs": [], + "source": [ + "# Get an authentication credential to access Azure resources and register features\n", + "if USE_CLI_AUTH:\n", + " # Use AZ CLI interactive browser authentication\n", + " !az login --use-device-code\n", + " from azure.identity import AzureCliCredential\n", + " credential = AzureCliCredential(additionally_allowed_tenants=['*'],)\n", + "elif \"AZURE_TENANT_ID\" in os.environ and \"AZURE_CLIENT_ID\" in os.environ and \"AZURE_CLIENT_SECRET\" in os.environ:\n", + " # Use Environment variable secret\n", + " from azure.identity import EnvironmentCredential\n", + " credential = EnvironmentCredential()\n", + "else:\n", + " # Try to use the default credential\n", + " from azure.identity import DefaultAzureCredential\n", + " credential = DefaultAzureCredential(\n", + " exclude_interactive_browser_credential=False,\n", + " additionally_allowed_tenants=['*'],\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac848162", + "metadata": {}, + "outputs": [], + "source": [ + "# Redis password\n", + "if \"REDIS_PASSWORD\" not in os.environ:\n", + " from azure.keyvault.secrets import SecretClient\n", + " vault_url = f\"https://{RESOURCE_PREFIX}kv.vault.azure.net\"\n", + " secret_client = SecretClient(vault_url=vault_url, credential=credential)\n", + " retrieved_secret = secret_client.get_secret('FEATHR-ONLINE-STORE-CONN').value\n", + " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e1a4a74d", + "metadata": {}, + "source": [ + "### 4.1 Initialize Feathr client" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18c3820d", + "metadata": {}, + "outputs": [], + "source": [ + "config_path = generate_config(\n", + " resource_prefix=RESOURCE_PREFIX,\n", + " project_name=PROJECT_NAME,\n", + " spark_config__spark_cluster=SPARK_CLUSTER,\n", + " spark_config__azure_synapse__dev_url=AZURE_SYNAPSE_URL,\n", + " spark_config__azure_synapse__pool_name=AZURE_SYNAPSE_SPARK_POOL,\n", + " spark_config__databricks__workspace_instance_url=SPARK_CONFIG__DATABRICKS__WORKSPACE_INSTANCE_URL,\n", + " databricks_cluster_id=DATABRICKS_CLUSTER_ID,\n", + ")\n", + "\n", + "with open(config_path, 'r') as f: \n", + " print(f.read())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "517c3357", + "metadata": {}, + "outputs": [], + "source": [ + "client = FeathrClient(config_path=config_path, credential=credential)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b13f1329", + "metadata": {}, + "source": [ + "### 4.2 Prepare Dataset\n", + "\n", + "To generate the data source for the features, let's get the Cognitive Search's AI skillset outputs from the Knowledge Store.\n", + "As we defined the Knowledge Store projection to be objects, the outputs are stored as Json records. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac2fd94a", + "metadata": {}, + "outputs": [], + "source": [ + "if \"spark\" in locals() or \"spark\" in globals():\n", + " spark.conf.set(f\"fs.azure.account.key.{STORAGE_NAME}.blob.core.windows.net\", STORAGE_KEY)\n", + "else:\n", + " from pyspark.sql import SparkSession\n", + " spark = (\n", + " SparkSession\n", + " .builder\n", + " .appName(\"feathr\")\n", + " .config(\n", + " \"spark.jars.packages\",\n", + " \",\".join([\n", + " \"org.apache.spark:spark-avro_2.12:3.3.0\",\n", + " \"io.delta:delta-core_2.12:2.1.1\",\n", + " \"org.apache.hadoop:hadoop-azure:3.3.0\",\n", + " \"com.microsoft.azure:azure-storage:8.6.6\",\n", + " ])\n", + " )\n", + " .config(f\"fs.azure.account.key.{STORAGE_NAME}.blob.core.windows.net\", STORAGE_KEY)\n", + " .config(\"spark.sql.extensions\", \"io.delta.sql.DeltaSparkSessionExtension\")\n", + " .config(\"spark.sql.catalog.spark_catalog\", \"org.apache.spark.sql.delta.catalog.DeltaCatalog\")\n", + " .config(\"spark.ui.port\", \"8080\") # Set ui port other than the default one (4040) so that feathr spark job doesn't fail. \n", + " .getOrCreate()\n", + " )\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "264b7a0a", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "# Read all the json records of the AI enrichment output\n", + "df = spark.read.option(\"recursiveFileLookup\", \"true\").json(f\"wasbs://{CONTAINER_NAME}output@{STORAGE_NAME}.blob.core.windows.net/\")\n", + "df.limit(5).toPandas()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a00d6dd6", + "metadata": {}, + "outputs": [], + "source": [ + "data_file_path = f\"{WORKING_DIR}/documents.parquet\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9069a239", + "metadata": {}, + "outputs": [], + "source": [ + "if Path(data_file_path).exists():\n", + " print(f\"Remove existing data file: {data_file_path}\")\n", + " shutil.rmtree(data_file_path)\n", + "print(f\"Write data file to: {data_file_path}\")\n", + "df.write.parquet(data_file_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02923a12", + "metadata": {}, + "outputs": [], + "source": [ + "# Upload files to cloud if needed\n", + "if client.spark_runtime == \"local\":\n", + " # In local mode, we can use the same data path as the source.\n", + " data_source_path = data_file_path\n", + "elif client.spark_runtime == \"databricks\" and is_databricks():\n", + " # If the notebook is running on databricks, we can use the same data path as the source.\n", + " data_source_path = data_file_path.replace(\"/dbfs\", \"dbfs:\")\n", + "else:\n", + " # Otherwise, upload the local file to the cloud storage (either dbfs or adls).\n", + " data_source_path = client.feathr_spark_launcher.upload_or_get_cloud_path(data_file_path) " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "61c0df09", + "metadata": {}, + "source": [ + "### 4.3 Define Features with UDF (User Defined Function)\n", + "\n", + "We preprocess the texts so that the later NLP models can consume them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5794eda7", + "metadata": {}, + "outputs": [], + "source": [ + "def preprocessing(df: DataFrame) -> DataFrame:\n", + " import pyspark.sql.functions as F\n", + " \n", + " # Any types of text preprocessing\n", + " return df.withColumn(\"text\", F.regexp_replace(\"text\", \"\\n\", \" \"))\n" + ] + }, + { + "cell_type": "markdown", + "id": "54efd59e", + "metadata": {}, + "source": [ + "Now, define features using the UDF." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c4bdb6b", + "metadata": {}, + "outputs": [], + "source": [ + "hdfs_source = HdfsSource(\n", + " name=\"ai_enrichment\",\n", + " path=data_source_path,\n", + " preprocessing=preprocessing,\n", + ")\n", + "\n", + "# key is required for the features from non-INPUT_CONTEXT source\n", + "key = TypedKey(\n", + " key_column=\"metadata_storage_name\",\n", + " key_column_type=ValueType.STRING,\n", + " description=\"Document name\",\n", + " full_name=f\"{PROJECT_NAME}.doc_name\",\n", + ")\n", + "\n", + "features = [\n", + " Feature(\n", + " name=\"f_text\",\n", + " key=key,\n", + " feature_type=STRING,\n", + " transform=\"text\",\n", + " ),\n", + "]\n", + "\n", + "feature_anchor = FeatureAnchor(\n", + " name=\"data_feature_anchor\",\n", + " source=hdfs_source,\n", + " features=features,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5079d649", + "metadata": {}, + "outputs": [], + "source": [ + "client.build_features(\n", + " anchor_list=[feature_anchor],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38393c24", + "metadata": {}, + "outputs": [], + "source": [ + "query = FeatureQuery(\n", + " feature_list=[\"f_text\"],\n", + " key=key,\n", + ")\n", + "\n", + "settings = ObservationSettings(\n", + " observation_path=data_source_path,\n", + ")\n", + "\n", + "client.get_offline_features(\n", + " observation_settings=settings,\n", + " feature_query=query,\n", + " # For more details, see https://feathr-ai.github.io/feathr/how-to-guides/feathr-job-configuration.html\n", + " execution_configurations=SparkExecutionConfiguration({\n", + " \"spark.feathr.outputFormat\": \"parquet\",\n", + " }),\n", + " output_path=\"./text_features.parquet\",\n", + ")\n", + "\n", + "client.wait_job_to_finish(timeout_sec=5000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8692f2df", + "metadata": {}, + "outputs": [], + "source": [ + "get_result_df(client, data_format=\"parquet\").head(5)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "49ea7f5b", + "metadata": {}, + "source": [ + "### 4.4 Register Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " client.register_features()\n", + "except Exception as e:\n", + " print(e) \n", + "print(client.list_registered_features(project_name=PROJECT_NAME))\n", + "# You can get the actual features too by calling client.get_features_from_registry(PROJECT_NAME)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1007a547", + "metadata": {}, + "source": [ + "### 4.5 Materialize Features to REDIS" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a3a9fd8", + "metadata": {}, + "outputs": [], + "source": [ + "FEATURE_TABLE_NAME = \"text_features\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90d0092d", + "metadata": {}, + "outputs": [], + "source": [ + "redis_sink = RedisSink(table_name=FEATURE_TABLE_NAME)\n", + "\n", + "settings = MaterializationSettings(\n", + " name=FEATURE_TABLE_NAME + \".job\", # job name\n", + " sinks=[redis_sink],\n", + " feature_names=[\"f_text\"],\n", + ")\n", + "\n", + "client.materialize_features(\n", + " settings=settings,\n", + " execution_configurations={\"spark.feathr.outputFormat\": \"parquet\"},\n", + " allow_materialize_non_agg_feature=True,\n", + ")\n", + "\n", + "client.wait_job_to_finish(timeout_sec=5000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2aa13fb1", + "metadata": {}, + "outputs": [], + "source": [ + "# Note, to get a single key, you may use client.get_online_features instead\n", + "materialized_feature_values = client.get_online_features(\n", + " feature_table=FEATURE_TABLE_NAME,\n", + " key=\"NYSE_LNKD_2015.PDF\",\n", + " feature_names=[\"f_text\"],\n", + ")\n", + "materialized_feature_values[0][:1000]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "a74fcbe7", + "metadata": {}, + "source": [ + "## 5. NLP Scenarios\n", + "\n", + "We use [HuggingFace Transformer package](https://huggingface.co/docs/transformers/installation) to demonstrate simple NLP scenarios with the materialized features. Specifically, we use [summarization and question-answering pipelines](https://huggingface.co/docs/transformers/main_classes/pipelines) that come with pre-trained models for the simplicity's sake.\n", + "\n", + "Firstly, install *Transformer* and *pyTorch* packages:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45821650", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -U transformers torch --extra-index-url https://download.pytorch.org/whl/cu116" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2215cae8", + "metadata": {}, + "outputs": [], + "source": [ + "from transformers import pipeline" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7263b727", + "metadata": {}, + "source": [ + "Now, let's use question-answering pipeline of the HuggingFace package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d38b589a", + "metadata": {}, + "outputs": [], + "source": [ + "qa_model = pipeline(\"question-answering\")\n", + "qa_model(\n", + " question=\"what is the LinkedIn's financial goal\",\n", + " context=materialized_feature_values[0],\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "6f6c5ee3", + "metadata": {}, + "source": [ + "With the summarization pipeline," + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3594c55f", + "metadata": {}, + "outputs": [], + "source": [ + "summarizer = pipeline(\"summarization\")\n", + "\n", + "# The pre-trained model only accepts 1024 tokens as input and thus we set truncation=True\n", + "summarizer(materialized_feature_values[0], truncation=True)\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "26b9927c", + "metadata": {}, + "source": [ + "## 6. Advanced Topics\n", + "\n", + "In this notebook, we have gone through how to utilize Azure Cognitive Search Skills to extract and translate texts from various formats of documents and use them with Feathr Feature Store for NLP scenarios.\n", + "\n", + "Here is a list of advanced topics we did not cover from this notebook:\n", + "\n", + "* [Deploy a model to AKS (Azure Kubernetes Service) via AzureML (Azure Machine Learning) SDK](https://learn.microsoft.com/en-us/azure/machine-learning/v1/how-to-deploy-azure-kubernetes-service?tabs=python).\n", + "* [Use Python and AI to generate searchable content from Azure blobs](https://learn.microsoft.com/en-us/azure/search/cognitive-search-tutorial-blob-python)\n", + "* [Enrich cognitive search index with custom classes](https://learn.microsoft.com/en-us/azure/cognitive-services/language-service/custom-text-classification/tutorials/cognitive-search?tabs=multi-classification%2CLanguage-studio)\n", + "* [Build and deploy a form recognizer custom skill](https://learn.microsoft.com/en-us/training/modules/build-form-recognizer-custom-skill-for-azure-cognitive-search/4-exercise-build-deploy)" + ] + }, + { + "cell_type": "markdown", + "id": "6e907bea", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "feathr", + "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.15" + }, + "vscode": { + "interpreter": { + "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/feathr_project/feathr/client.py b/feathr_project/feathr/client.py index 24954cba8..aa4955e23 100644 --- a/feathr_project/feathr/client.py +++ b/feathr_project/feathr/client.py @@ -309,7 +309,7 @@ def _get_registry_client(self): """ return self.registry._get_registry_client() - def get_online_features(self, feature_table, key, feature_names): + def get_online_features(self, feature_table: str, key: Any, feature_names: List[str]): """Fetches feature value for a certain key from a online feature table. Args: @@ -330,7 +330,7 @@ def get_online_features(self, feature_table, key, feature_names): res = self.redis_client.hmget(redis_key, *feature_names) return self._decode_proto(res) - def multi_get_online_features(self, feature_table, keys, feature_names): + def multi_get_online_features(self, feature_table: str, keys: List[Any], feature_names: List[str]): """Fetches feature value for a list of keys from a online feature table. This is the batch version of the get API. Args: From 47959144ba4b56181d1ed2a2c73a549e6315c5b1 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Thu, 5 Jan 2023 03:24:44 +0000 Subject: [PATCH 20/22] Update fraud detection sample Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/fraud_detection_demo.ipynb | 407 +++++++++--------- feathr_project/test/samples/test_notebooks.py | 3 + 2 files changed, 216 insertions(+), 194 deletions(-) diff --git a/docs/samples/fraud_detection_demo.ipynb b/docs/samples/fraud_detection_demo.ipynb index ddcb7bc21..c0f4fb915 100644 --- a/docs/samples/fraud_detection_demo.ipynb +++ b/docs/samples/fraud_detection_demo.ipynb @@ -13,37 +13,49 @@ "source": [ "# Feathr Fraud Detection Sample\n", "\n", - "This notebook illustrates the use of Feature Store to create a model that predicts the fraud status of transactions based on the user account data and trasaction data. All the data that was used in the notebook can be found here: https://github.com/microsoft/r-server-fraud-detection.\n", + "This notebook illustrates the use of Feature Store to create a model that predicts the fraud status of transactions based on the user account data and trasaction data. The main focus of this notebook is to depict:\n", + "* How a feature designer can define heterogenious features from different data sources (user account data and transaction data) with different keys by using Feathr, and\n", + "* How a feature consumer can extract features using multiple `FeatureQuery`.\n", "\n", + "The sample fraud transaction datasets that are used in the notebook can be found here: https://github.com/microsoft/r-server-fraud-detection.\n", "\n", - "In the following Notebook, we \n", - "1. Install the latest Feathr code (to include some unreleased features) \n", - "2. Define Environment Variables & `yaml_config` Settings \n", - "3. Create `FeathrClient` and Define `FeatureAnchor`\n", - "4. `build_features` and `get_offline_features`\n", - "5. Visualize features and train Fraud Detection Model\n", - "6. `materialize_features` and `get_online_features`" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "0b51153e-40dd-43d5-9d3a-501534156e6d", - "showTitle": false, - "title": "" - } - }, - "source": [ - "## Setup Feathr Developer Environment" + "The outline of the notebook is as follows: \n", + "1. Setup Feathr environment\n", + "2. Initialize Feathr client \n", + "3. Define features\n", + "4. Build features and extract offline features\n", + "5. Build a fraud detection model\n", + "6. Materialize features" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - ">Prior to running the notebook, if you have not deployed all the required resources, please refer to the guide here and follow the steps to do so: https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-arm.html" + "## 1. Setup Feathr Environment\n", + "\n", + "### Deploy Necessary Azure Resources to run Feathr Feature Store\n", + "\n", + "Prior to running the notebook, if you have not deployed all the required resources, please refer to the guide here and follow the steps to do so: https://feathr-ai.github.io/feathr/how-to-guides/azure-deployment-arm.html\n", + "\n", + "### Access to Resources\n", + "To run the cells below, you need additional permissions for your managed identity to access the keyvault and the Storage Account. You may run the following lines of command in the Cloud Shell in order to grant yourself the access.\n", + "\n", + "```\n", + "userId=\n", + "resource_prefix=\n", + "synapse_workspace_name=\"${resource_prefix}syws\"\n", + "keyvault_name=\"${resource_prefix}kv\"\n", + "objectId=$(az ad user show --id $userId --query id -o tsv)\n", + "az keyvault update --name $keyvault_name --enable-rbac-authorization false\n", + "az keyvault set-policy -n $keyvault_name --secret-permissions get list --object-id $objectId\n", + "az role assignment create --assignee $userId --role \"Storage Blob Data Contributor\"\n", + "az synapse role assignment create --workspace-name $synapse_workspace_name --role \"Synapse Contributor\" --assignee $userId\n", + "```\n", + "\n", + "### Install Python Packages\n", + "\n", + "Uncomment following cell and run it to install Feathr python package and necessary dependencies." ] }, { @@ -63,6 +75,13 @@ "# !pip install \"git+https://github.com/feathr-ai/feathr.git#subdirectory=feathr_project&egg=feathr[notebook]\" " ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Initialize Feathr Client" + ] + }, { "cell_type": "code", "execution_count": null, @@ -80,6 +99,7 @@ "import os\n", "from pathlib import Path\n", "\n", + "import numpy as np\n", "import pandas as pd\n", "\n", "import feathr\n", @@ -161,26 +181,6 @@ " os.environ[\"DATABRICKS_WORKSPACE_TOKEN_VALUE\"] = DATABRICKS_WORKSPACE_TOKEN_VALUE" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Permission\n", - "To run the cells below, you need additional permission: permission to your managed identity to access the keyvault, and permission to the user to access the Storage Blob. Run the following lines of command in the Cloud Shell in order to grant yourself the access.\n", - "\n", - "```\n", - "userId=\n", - "resource_prefix=\n", - "synapse_workspace_name=\"${resource_prefix}syws\"\n", - "keyvault_name=\"${resource_prefix}kv\"\n", - "objectId=$(az ad user show --id $userId --query id -o tsv)\n", - "az keyvault update --name $keyvault_name --enable-rbac-authorization false\n", - "az keyvault set-policy -n $keyvault_name --secret-permissions get list --object-id $objectId\n", - "az role assignment create --assignee $userId --role \"Storage Blob Data Contributor\"\n", - "az synapse role assignment create --workspace-name $synapse_workspace_name --role \"Synapse Contributor\" --assignee $userId\n", - "```" - ] - }, { "cell_type": "code", "execution_count": null, @@ -230,6 +230,13 @@ " os.environ['REDIS_PASSWORD'] = retrieved_secret.split(\",\")[1].split(\"password=\", 1)[1]" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Generate a config file" + ] + }, { "cell_type": "code", "execution_count": null, @@ -272,7 +279,7 @@ } }, "source": [ - "## Initialize Feathr Client" + "### Initialize Feathr client" ] }, { @@ -295,11 +302,20 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Prepare Datasets\n", + "## 3. Define Features\n", + "\n", + "### Prepare datasets\n", + "\n", + "We prepare the fraud detection dataset as follows:\n", "\n", "1. Download Account info data, fraud transactions data, and untagged transactions data.\n", - "2. Merge two transactions data (fraud and untagged) into one\n", - "3. Upload data files to cloud so that the target cluster can consume" + "2. Tag transaction data based on the fraud transactions data.\n", + " 1. Aggregate the Fraud table on the account level, creating a start and end datetime. \n", + " 2. Join this data with the untagged data.\n", + " 3. Tag the data: `is_fraud = 0` for non fraud, `1` for fraud. \n", + "3. Upload data files to cloud so that the Feathr's target cluster can consume.\n", + "\n", + "To learn more about the fraud detection scenario as well as the dataset source we use and the method we tag the transactions, please see [here](https://microsoft.github.io/r-server-fraud-detection/data-scientist.html)." ] }, { @@ -345,14 +361,72 @@ "metadata": {}, "outputs": [], "source": [ - "# Concat fraud and obs transactions\n", + "# Load datasets\n", "fraud_df = pd.read_csv(fraud_transactions_file_path)\n", - "fraud_df[\"fraud_tag\"] = \"Fraud\"\n", - "obs_df = pd.read_csv(obs_transactions_file_path)\n", - "obs_df[\"fraud_tag\"] = \"Unknown\"\n", + "obs_df = pd.read_csv(obs_transactions_file_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Combine transactionDate and transactionTime into one column. E.g. \"20130903\", \"013641\" -> \"20130903 013641\"\n", + "fraud_df[\"timestamp\"] = fraud_df[\"transactionDate\"].astype(str) + \" \" + fraud_df[\"transactionTime\"].astype(str).str.zfill(6)\n", + "obs_df[\"timestamp\"] = obs_df[\"transactionDate\"].astype(str) + \" \" + obs_df[\"transactionTime\"].astype(str).str.zfill(6)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this step, labels are added to the transactional data by referencing the transaction-level fraud data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# For each user in the fraud transaction data, get the timestamp range that the fraud transactions were happened. \n", + "fraud_labels_df = fraud_df.groupby(\"accountID\").agg({\"timestamp\": ['min', 'max']})\n", + "fraud_labels_df.columns = [\"_\".join(col) for col in fraud_labels_df.columns.values]\n", + "fraud_labels_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Combine fraud and untagged transaction data to generate the tagged transaction data.\n", + "transactions_df = pd.concat([fraud_df, obs_df], ignore_index=True)\n", + "transactions_df = transactions_df.merge(\n", + " fraud_labels_df,\n", + " on=\"accountID\",\n", + " how=\"outer\",\n", + ")\n", + "# is_fraud = 0 if the transaction is not fraud. Otherwise (if it is a fraud), is_fraud = 1.\n", + "transactions_df[\"is_fraud\"] = np.logical_and(\n", + " transactions_df[\"timestamp_min\"] <= transactions_df[\"timestamp\"],\n", + " transactions_df[\"timestamp\"] <= transactions_df[\"timestamp_max\"],\n", + ").astype(int)\n", "\n", + "transactions_df.drop_duplicates(inplace=True)\n", + "transactions_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Save the tagged transaction data into file\n", "transactions_file_path = f\"{WORKING_DIR}/transactions.csv\"\n", - "transactions_df = pd.concat([fraud_df, obs_df], ignore_index=True)\n", "transactions_df.to_csv(transactions_file_path, index=False)" ] }, @@ -389,8 +463,6 @@ } }, "source": [ - "## Define Features\n", - "\n", "Now, we define following features:\n", "- Account features: Account-level features that will be joined to observation data on accountID\n", "- Transaction features: The features that will be joined to observation data on transactionID\n", @@ -411,7 +483,7 @@ } }, "source": [ - "### Account Features" + "### Define account features" ] }, { @@ -434,7 +506,6 @@ " \"\"\"Drop rows with missing values in the account info dataset.\"\"\"\n", " return df.select(\n", " \"accountID\",\n", - " \"transactionDate\",\n", " \"accountCountry\",\n", " \"isUserRegistered\",\n", " \"numPaymentRejects1dPerUser\",\n", @@ -445,8 +516,6 @@ "account_info_source = HdfsSource(\n", " name=\"account_data\",\n", " path=account_info_source_path,\n", - " event_timestamp_column=\"transactionDate\",\n", - " timestamp_format=\"yyyyMMdd\",\n", " preprocessing=account_dropna,\n", ")" ] @@ -516,7 +585,7 @@ } }, "source": [ - "### Transaction Features" + "### Define transaction features" ] }, { @@ -539,20 +608,19 @@ " \"\"\"Drop rows with missing values in the transactions dataset.\"\"\"\n", " return df.dropna(subset=[\n", " \"accountID\",\n", - " \"transactionDate\",\n", " \"transactionID\",\n", " \"transactionCurrencyCode\",\n", " \"transactionAmount\",\n", " \"transactionTime\",\n", " \"ipCountryCode\",\n", + " \"timestamp\",\n", " ])\n", "\n", - "\n", "transactions_source = HdfsSource(\n", " name=\"transaction_data\",\n", " path=transactions_source_path,\n", - " event_timestamp_column=\"transactionDate\",\n", - " timestamp_format=\"yyyyMMdd\",\n", + " event_timestamp_column=\"timestamp\",\n", + " timestamp_format=\"yyyyMMdd HHmmss\",\n", " preprocessing=transaction_dropna,\n", ")" ] @@ -624,7 +692,7 @@ } }, "source": [ - "### Transaction Aggregated Features" + "### Define transaction aggregated-features" ] }, { @@ -708,7 +776,7 @@ } }, "source": [ - "### Derived Features" + "### Define derived features" ] }, { @@ -746,7 +814,9 @@ } }, "source": [ - "## Build and Get Features" + "## 4. Build Features and Extract Offline Features\n", + "\n", + "Now, let's build the features." ] }, { @@ -783,6 +853,13 @@ "derived_feature_names = [feat.name for feat in derived_features]" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To extract the offline feature values from the features that have different keys, we use multiple `FeatureQuery` objects." + ] + }, { "cell_type": "code", "execution_count": null, @@ -813,8 +890,8 @@ " \n", "settings = ObservationSettings(\n", " observation_path=transactions_source_path,\n", - " event_timestamp_column=\"transactionDate\",\n", - " timestamp_format=\"yyyyMMdd\",\n", + " event_timestamp_column=\"timestamp\",\n", + " timestamp_format=\"yyyyMMdd HHmmss\",\n", ")\n", " \n", "client.get_offline_features(\n", @@ -836,12 +913,10 @@ " account_feature_names\n", " + transactions_feature_names\n", " + derived_feature_names\n", - " + [\"accountID\", \"transactionID\", \"fraud_tag\"]\n", + " + [\"is_fraud\"]\n", "]\n", "\n", - "# Data cleaning: Remove the records if the account does not exist in the account info dataset\n", - "df.dropna(subset=[\"is_user_registered\"], inplace=True)\n", - "\n", + "df.dropna(inplace=True)\n", "df.head(5)" ] }, @@ -858,7 +933,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Feature Visualization" + "## 5. Build a Fraud Detection Model\n", + "\n", + "We use [Random Forest Classifier](https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html) to build a fraud detection model." ] }, { @@ -868,9 +945,23 @@ "outputs": [], "source": [ "import plotly.express as px\n", - "\n", - "\n", - "NUM_SAMPLES_TO_PLOT = 10000" + "from sklearn.ensemble import RandomForestClassifier\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.metrics import (\n", + " confusion_matrix,\n", + " f1_score,\n", + " precision_score,\n", + " recall_score,\n", + " PrecisionRecallDisplay,\n", + ")\n", + "from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Understand the dataset" ] }, { @@ -879,10 +970,13 @@ "metadata": {}, "outputs": [], "source": [ + "# plot only sub-samples for simplicity\n", + "NUM_SAMPLES_TO_PLOT = 5000\n", + "\n", "fig = px.scatter_matrix(\n", " df[:NUM_SAMPLES_TO_PLOT],\n", " dimensions=account_feature_names + transactions_feature_names + derived_feature_names,\n", - " color=\"fraud_tag\",\n", + " color=\"is_fraud\",\n", " title=\"Scatter matrix of transaction dataset\",\n", ")\n", "fig.update_traces(diagonal_visible=False, marker_size=3)\n", @@ -894,49 +988,20 @@ "fig.show()" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Build Fraud Detection Model\n", - "\n", - "In this notebook, we train one-class Support Vector Machine (SVM) to score the transactions, where the score results can be used to determine if the transactions are fraud or not.\n", - "\n", - "### Feature Preprocessing\n", - "\n", - "Before we input the features to the model, we convert categorical features into neumeric vectors (using a simple one-hot encoding technique) and do standard scaling all the features." - ] - }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", - "from sklearn.preprocessing import OneHotEncoder, StandardScaler" + "df[\"is_fraud\"].value_counts()" ] }, { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Split fraud (train) and observation (test) datasets\n", - "fraud_df = df[df[\"fraud_tag\"]==\"Fraud\"].drop([\"accountID\", \"transactionID\", \"fraud_tag\"], axis=\"columns\") \n", - "obs_df = df[df[\"fraud_tag\"]==\"Unknown\"].drop([\"accountID\", \"transactionID\", \"fraud_tag\"], axis=\"columns\") \n", - "print(f\"Num fraud samples = {len(fraud_df)}\", f\"Num untagged samples = {len(obs_df)}\", sep=\"\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "# Feature names to encode\n", - "enc_feature_names = [\"account_country\", \"is_user_registered\", \"transaction_ip_country_code\", \"transaction_currency_code\"]" + "### Train and test the model" ] }, { @@ -945,25 +1010,8 @@ "metadata": {}, "outputs": [], "source": [ - "# Define and fit one-hot encoder\n", - "enc = OneHotEncoder(handle_unknown=\"ignore\").fit(fraud_df[enc_feature_names])\n", - "\n", - "fraud_features = np.concatenate(\n", - " (\n", - " # Encoded features\n", - " enc.transform(fraud_df[enc_feature_names]).toarray(),\n", - " # Other features that don't need to be encoded\n", - " fraud_df.drop(enc_feature_names, axis=\"columns\").fillna(0).to_numpy(),\n", - " \n", - " ),\n", - " axis=1,\n", - ")\n", - "\n", - "# Define and fit standard scaler\n", - "scaler = StandardScaler().fit(fraud_features)\n", - "\n", - "fraud_features = scaler.transform(fraud_features)\n", - "print(f\"A sample of fraud feature:\\n{fraud_features[0]}\\nData shape = {fraud_features.shape}\")" + "y = df[\"is_fraud\"].astype(int).to_numpy()\n", + "X_df = df.drop(\"is_fraud\", axis=\"columns\")" ] }, { @@ -972,32 +1020,16 @@ "metadata": {}, "outputs": [], "source": [ - "obs_features = np.concatenate(\n", + "# We convert categorical features into integer values by using One-hot-encoding\n", + "categorical_feature_names = [\"account_country\", \"transaction_ip_country_code\", \"transaction_currency_code\"]\n", + "X = np.concatenate(\n", " (\n", - " # Encoded features\n", - " enc.transform(obs_df[enc_feature_names]).toarray(),\n", - " # Other features that don't need to be encoded\n", - " obs_df.drop(enc_feature_names, axis=\"columns\").fillna(0).to_numpy(),\n", - " \n", + " OneHotEncoder().fit_transform(df[categorical_feature_names]).toarray(),\n", + " X_df.drop(categorical_feature_names, axis=\"columns\").to_numpy(),\n", " ),\n", " axis=1,\n", ")\n", - "obs_features = scaler.transform(obs_features)\n", - "print(f\"A sample of observation feature:\\n{obs_features[0]}\\nData shape = {obs_features.shape}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "application/vnd.databricks.v1+cell": { - "inputWidgets": {}, - "nuid": "7fff1ac7-90d1-469b-a54c-397904417796", - "showTitle": false, - "title": "" - } - }, - "source": [ - "### Model Training" + "X.shape" ] }, { @@ -1006,16 +1038,9 @@ "metadata": {}, "outputs": [], "source": [ - "from sklearn.svm import OneClassSVM\n", - "\n", - "clf = OneClassSVM(nu=0.1, kernel=\"rbf\", gamma=0.1).fit(fraud_features)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Data Scoring" + "X_train, X_test, y_train, y_test = train_test_split(\n", + " X, y, test_size=0.33, random_state=42, stratify=y,\n", + ")" ] }, { @@ -1024,8 +1049,9 @@ "metadata": {}, "outputs": [], "source": [ - "fraud_feature_scores = clf.score_samples(fraud_features)\n", - "fraud_feature_scores" + "print(f\"\"\"[Number of fraud samples / total number of samples]\n", + "training set: {y_train.sum()} / {len(y_train)} \n", + "test set: {y_test.sum()} / {len(y_test)}\"\"\")" ] }, { @@ -1034,15 +1060,7 @@ "metadata": {}, "outputs": [], "source": [ - "obs_feature_scores = clf.score_samples(obs_features)\n", - "obs_feature_scores" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Result Visualization" + "clf = RandomForestClassifier(n_estimators=100, random_state=42).fit(X_train, y_train)" ] }, { @@ -1051,7 +1069,7 @@ "metadata": {}, "outputs": [], "source": [ - "from sklearn.manifold import TSNE" + "clf.score(X_test, y_test)" ] }, { @@ -1060,19 +1078,15 @@ "metadata": {}, "outputs": [], "source": [ - "# We subsample observation transaction data for visualization\n", - "obs_sample_idx = np.random.choice(len(obs_features), size=NUM_SAMPLES_TO_PLOT, replace=False)\n", - "obs_sample_idx" + "y_pred = clf.predict(X_test)\n", + "y_pred" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "scores = np.concatenate([fraud_feature_scores, obs_feature_scores[obs_sample_idx]], axis=0)\n", - "scores.shape" + "To measure the performance, we use recall, precision and F1 score that handle imbalanced data better." ] }, { @@ -1081,8 +1095,10 @@ "metadata": {}, "outputs": [], "source": [ - "features = np.concatenate([fraud_features, obs_features[obs_sample_idx]], axis=0)\n", - "features.shape" + "display = PrecisionRecallDisplay.from_predictions(\n", + " y_test, y_pred, name=\"HistGradientBoostingClassifier\"\n", + ")\n", + "_ = display.ax_.set_title(\"Fraud Detection Precision-Recall Curve\")" ] }, { @@ -1091,8 +1107,13 @@ "metadata": {}, "outputs": [], "source": [ - "tsne = TSNE(n_components=2, perplexity=20, n_iter=300)\n", - "tsne_results = tsne.fit_transform(features)" + "precision = precision_score(y_test, y_pred)\n", + "recall = recall_score(y_test, y_pred)\n", + "f1 = f1_score(y_test, y_pred)\n", + "\n", + "print(f\"\"\"Precision: {precision},\n", + "Recall: {recall},\n", + "F1: {f1}\"\"\")" ] }, { @@ -1101,12 +1122,7 @@ "metadata": {}, "outputs": [], "source": [ - "fig = px.scatter(x=tsne_results[:, 0], y=tsne_results[:, 1], color=scores)\n", - "fig.update_traces(marker_size=5, marker_opacity=0.5)\n", - "fig.update_layout(\n", - " width=800,\n", - " height=800,\n", - ")" + "confusion_matrix(y_test, y_pred)" ] }, { @@ -1202,7 +1218,10 @@ "source": [ "if SCRAP_RESULTS:\n", " import scrapbook as sb\n", - " sb.glue(\"materialized_feature_values\", materialized_feature_values)" + " sb.glue(\"materialized_feature_values\", materialized_feature_values)\n", + " sb.glue(\"precision\", precision)\n", + " sb.glue(\"recall\", recall)\n", + " sb.glue(\"f1\", f1)" ] }, { @@ -1236,7 +1255,7 @@ "widgets": {} }, "kernelspec": { - "display_name": "feathr", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1250,11 +1269,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8 (main, Nov 24 2022, 14:13:03) [GCC 11.2.0]" + "version": "3.8.15" }, "vscode": { "interpreter": { - "hash": "ddb0e38f168d5afaa0b8ab4851ddd8c14364f1d087c15de6ff2ee5a559aec1f2" + "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" } } }, diff --git a/feathr_project/test/samples/test_notebooks.py b/feathr_project/test/samples/test_notebooks.py index f462082cb..5f33a7348 100644 --- a/feathr_project/test/samples/test_notebooks.py +++ b/feathr_project/test/samples/test_notebooks.py @@ -99,6 +99,9 @@ def test__fraud_detection_demo(config_path, tmp_path): outputs = nb.scraps assert outputs["materialized_feature_values"].data == pytest.approx([False, 0, 9, 239.0, 1, 1, 239.0, 33816.0], abs=1.) + assert outputs["precision"].data > 0.5 + assert outputs["recall"].data > 0.5 + assert outputs["f1"].data > 0.5 @pytest.mark.notebooks From 9285479bf8d32125bf687a70e8c1c161f5ee8fa8 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Mon, 9 Jan 2023 23:17:35 +0000 Subject: [PATCH 21/22] Fix Precision/recall graph of the fraud detection sample notebook Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/fraud_detection_demo.ipynb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/samples/fraud_detection_demo.ipynb b/docs/samples/fraud_detection_demo.ipynb index c0f4fb915..85e4b91b3 100644 --- a/docs/samples/fraud_detection_demo.ipynb +++ b/docs/samples/fraud_detection_demo.ipynb @@ -1082,6 +1082,16 @@ "y_pred" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "y_prob = clf.predict_proba(X_test)\n", + "y_prob" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -1096,7 +1106,7 @@ "outputs": [], "source": [ "display = PrecisionRecallDisplay.from_predictions(\n", - " y_test, y_pred, name=\"HistGradientBoostingClassifier\"\n", + " y_test, y_prob[:, 1], name=\"RandomForestClassifier\"\n", ")\n", "_ = display.ax_.set_title(\"Fraud Detection Precision-Recall Curve\")" ] From 12a29cceb9d6920b4dd9eb6707147f810630b499 Mon Sep 17 00:00:00 2001 From: Jun Ki Min <42475935+loomlike@users.noreply.github.com> Date: Mon, 9 Jan 2023 18:41:24 -0800 Subject: [PATCH 22/22] Change wait for job to finish to be 5000 sec Signed-off-by: Jun Ki Min <42475935+loomlike@users.noreply.github.com> --- docs/samples/product_recommendation_demo_advanced.ipynb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/samples/product_recommendation_demo_advanced.ipynb b/docs/samples/product_recommendation_demo_advanced.ipynb index a0fc34988..8afffffc8 100644 --- a/docs/samples/product_recommendation_demo_advanced.ipynb +++ b/docs/samples/product_recommendation_demo_advanced.ipynb @@ -1208,7 +1208,7 @@ ")\n", "\n", "client.materialize_features(settings, allow_materialize_non_agg_feature=True)\n", - "client.wait_job_to_finish(timeout_sec=1000)" + "client.wait_job_to_finish(timeout_sec=5000)" ] }, { @@ -1329,7 +1329,7 @@ "widgets": {} }, "kernelspec": { - "display_name": "feathr", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -1343,11 +1343,11 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.8.5 (default, Jan 27 2021, 15:41:15) \n[GCC 9.3.0]" }, "vscode": { "interpreter": { - "hash": "e34a1a57d2e174682770a82d94a178aa36d3ccfaa21227c5d2308e319b7ae532" + "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" } } },