{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Part 3: Training and deploying an Amazon Fraud Detector model" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

💡 NOTE

\n", "In order to execute this notebook, you must first execute the previous notebook 2-afd-model-setup.ipynb.\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Overview \n", "\n", "* [Notebook 1: Data Preparation, Process, and Store Features](./1-data-analysis-prep.ipynb)\n", "* [Notebook 2: Amazon Fraud Detector Model Setup](./2-afd-model-setup.ipynb)\n", "* **[Notebook 3: Model training, deployment, real-time and batch inference](./3-afd-model-train-deploy.ipynb)**\n", " * **[Introduction](#intro)**\n", " * **[Notebook Setup](#setup)**\n", " * **[Set region, boto3 and SageMaker SDK variables](#setup2)**\n", " * **[Create & Train your Model](#train)**\n", " * **[Check model performance and activate](#activate)**\n", " * **[Create rules and detector](#detector)**\n", " * **[Make Predictions](#predictions)**\n", " * **[Conclusion](#conclusion)**\n", "* [Notebook 4: Create an end-to-end pipeline](./4-afd-pipeline.ipynb)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. Introduction \n", "___\n", "overview" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Amazon Fraud Detector Online Fraud Insights is a supervised machine learning model designed to detect a variety of online fraud and risks. Because the model is supervised, it requires historical examples of fraudulent and legitimate events to train the model.\n", "\n", "The Online Fraud Insights model uses an ensemble of machine-learning algorithms for data enrichment, transformation, and fraud classification. As part of the model training process, Online Fraud Insights enriches raw data elements like IP address and BIN number with 3rd party data such as the geo-location of the IP address or the issuing bank for a credit card. In addition to 3rd party data, Online Fraud Insights uses deep learning algorithms leveraging fraud patterns seen at Amazon and AWS. These fraud patterns become input features to your model using a gradient tree boosting algorithm. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As part of this notebook, we will create and train an Amazon Fraud Detector [Online Fraud Insights](https://docs.aws.amazon.com/frauddetector/latest/ug/online-fraud-insights.html) model. Once a model is created and trained, we can create a detector which can be used to perform inferences.\n", "\n", "A detector is a rules-based categorization engine that predicts predefined outcomes based on user configuration. For this tutorial, you define the model score thresholds as rules for the detector.\n", "\n", "Models can either be trained within Amazon Fraud Detector using custom user data or they can be accessed from precreated Amazon Sagemaker endpoints.\n", "\n", "The high-level configuration flow is depicted in the following diagram.\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Setup \n", "----\n", "overview\n", "\n", "As part of setup, we will retrieve those variables in order to use them here. We will also import some required libraries in this section." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from IPython.core.display import display, HTML\n", "from IPython.display import clear_output, JSON\n", "\n", "display(HTML(\"\"))\n", "# ------------------------------------------------------------------\n", "\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import pandas as pd\n", "import os\n", "import sys\n", "import time\n", "import json\n", "import uuid \n", "from datetime import datetime\n", "import boto3\n", "import sagemaker\n", "\n", "pd.set_option('display.max_rows', 500)\n", "pd.set_option('display.max_columns', 500)\n", "pd.set_option('display.width', 1000)\n", "\n", "# -- sklearn --\n", "from sklearn.metrics import roc_curve, roc_auc_score, auc, roc_auc_score\n", "%matplotlib inline" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The previous notebook [2-afd-model-setup.ipynb](./2-afd-model-setup.ipynb) defines and saves a few variables into the Jupyter cache using `%store` magic command. We will retrieve those variables to be used in this notebook. " ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "data": { "text/html": [ "

Retrieved values:

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/json": { "labelSchema": { "labelMapper": { "FRAUD": [ "fraud" ], "LEGIT": [ "legit" ] } }, "modelVariables": [ "ip_address", "email_address", "user_agent", "customer_name", "phone_number", "customer_city", "customer_postal", "customer_state", "customer_address" ] }, "text/plain": [ "" ] }, "metadata": { "application/json": { "expanded": false, "root": "root" } }, "output_type": "display_data" } ], "source": [ "# Retrieve cached variables\n", "\n", "%store -r ENTITY_TYPE\n", "%store -r ENTITY_DESC\n", "%store -r EVENT_TYPE\n", "%store -r EVENT_DESC\n", "%store -r MODEL_NAME\n", "%store -r MODEL_DESC\n", "%store -r DETECTOR_NAME\n", "%store -r DETECTOR_DESC\n", "%store -r S3_FILE_LOC\n", "%store -r trainingDataSchema\n", "%store -r eventVariables\n", "\n", "display(HTML(f\"

Retrieved values:

  • {ENTITY_TYPE}
  • {ENTITY_DESC}
  • {EVENT_TYPE}
  • {EVENT_DESC}
  • {MODEL_NAME}
  • {MODEL_DESC}
  • {DETECTOR_NAME}
  • {DETECTOR_DESC}
  • {S3_FILE_LOC}

\"))\n", "display(JSON(trainingDataSchema))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Set region, boto3 and SageMaker SDK variables \n", "---\n", "overview" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We will initialize a Fraud Detector, S3 and Sagemaker Boto3 client objects." ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Using AWS Region: us-east-2\n" ] } ], "source": [ "#You can change this to a region of your choice\n", "region = sagemaker.Session().boto_region_name\n", "print(\"Using AWS Region: {}\".format(region))" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "boto3.setup_default_session(region_name=region)\n", "\n", "boto_session = boto3.Session(region_name=region)\n", "\n", "# -- initialize S3 Client\n", "s3_client = boto3.client('s3', region_name=region)\n", "\n", "# -- initialize the AFD client \n", "client = boto3.client('frauddetector')\n", "\n", "sagemaker_boto_client = boto_session.client('sagemaker')\n", "\n", "sagemaker_session = sagemaker.session.Session(\n", " boto_session=boto_session,\n", " sagemaker_client=sagemaker_boto_client)\n", "\n", "# -- suffix is appended to detector and model name for uniqueness \n", "sufx = datetime.now().strftime(\"%Y%m%d\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We will get the SageMaker Execution Role " ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "SageMaker Role: AmazonSageMaker-ExecutionRole-20201030T135016\n" ] } ], "source": [ "print('SageMaker Role:', sagemaker.get_execution_role().split('/')[-1])\n", "\n", "ARN_ROLE = sagemaker.get_execution_role()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4. Create & Train your Model \n", "-----\n", "overview\n", "\n", "An Amazon Fraud Detector model version is created by calling the [CreateModel](https://docs.aws.amazon.com/frauddetector/latest/api/API_CreateModel.html) and [CreateModelVersion](https://docs.aws.amazon.com/frauddetector/latest/api/API_CreateModelVersion.html) operations. \n", "\n", "* CreateModel initiates the model, which acts as a container for your model versions. \n", "* CreateModelVersion starts the training process, which results in a specific version of the model. A new version of the solution is created each time you call CreateModelVersion.\n", "\n", "As stated earlier, we will initialize a model of `modelType = ONLINE_FRAUD_INSIGHTS`.\n" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/html": [ "

Initialize Model

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/json": { "ResponseMetadata": { "HTTPHeaders": { "connection": "keep-alive", "content-length": "2", "content-type": "application/x-amz-json-1.1", "date": "Thu, 13 May 2021 16:02:42 GMT", "x-amzn-requestid": "fbd96642-3943-424b-a836-fe17784c0255" }, "HTTPStatusCode": 200, "RequestId": "fbd96642-3943-424b-a836-fe17784c0255", "RetryAttempts": 0 } }, "text/plain": [ "" ] }, "metadata": { "application/json": { "expanded": false, "root": "root" } }, "output_type": "display_data" } ], "source": [ "# -- create AFD model --\n", "response = client.create_model(\n", " description = MODEL_DESC,\n", " eventTypeName = EVENT_TYPE,\n", " modelId = MODEL_NAME,\n", " modelType = 'ONLINE_FRAUD_INSIGHTS')\n", "\n", "# Display output\n", "display(HTML(\"

Initialize Model

\"))\n", "display(JSON(response))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we will train our first version using the `CreateModelVersion` API. `TrainingDataSource` and `ExternalEventsDetail` specify the source and Amazon S3 location of the training data set. `TrainingDataSchema` specifies how Amazon Fraud Detector should interpret the training data, specifically which event variables to include and how to classify the event labels.\n", "\n", "

💡 NOTE

\n", "Running the code cell below will kick-off the training process asynchronously. Training can take somewhere from 30 mins to 60 mins. At anypoint during the training, you can cancel the training by calling UpdateModelVersionStatus and updating the status to TRAINING_CANCELLED.\n", "
\n", "\n", "We will also need to provide an IAM Role ARN to the create_model_version call. This is different than the IAM Role that we have been using so far. The `dataAccessRoleArn` is an IAM role that should be assumable by Amazon Fraud Detector, so you will need to create a \"New\" IAM Role and assign it S3 read/write policy with a trust relationship with Amazon Fraud Detector. Here's what the IAM Policy looks like [**Note**: replace the bucket name appropriately]\n", "\n", "```json\n", "{\n", " \"Version\": \"2012-10-17\",\n", " \"Statement\": [\n", " {\n", " \"Action\": [\n", " \"s3:ListBucket\",\n", " \"s3:GetBucketLocation\"\n", " ],\n", " \"Effect\": \"Allow\",\n", " \"Resource\": [\n", " \"arn:aws:s3:::YOUR_BUCKET_NAME\"\n", " ]\n", " },\n", " {\n", " \"Action\": [\n", " \"s3:GetObject\"\n", " ],\n", " \"Effect\": \"Allow\",\n", " \"Resource\": [\n", " \"arn:aws:s3:::YOUR_BUCKET_NAME/*\"\n", " ]\n", " }\n", " ]\n", "}\n", "```" ] }, { "cell_type": "code", "execution_count": 63, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Stored 'DATA_ACCESS_ROLE_ARN' (str)\n" ] } ], "source": [ "# Replace with the Role ARN\n", "\n", "DATA_ACCESS_ROLE_ARN=\"arn:aws:iam::965425568475:role/service-role/AmazonFraudDetector-DataAccessRole-1620923454652\"\n", "%store DATA_ACCESS_ROLE_ARN\n" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "data": { "text/html": [ "

Model training

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/json": { "ResponseMetadata": { "HTTPHeaders": { "connection": "keep-alive", "content-length": "132", "content-type": "application/x-amz-json-1.1", "date": "Thu, 13 May 2021 19:33:30 GMT", "x-amzn-requestid": "3dba7bcc-ea38-4371-b979-710fe3d0063d" }, "HTTPStatusCode": 200, "RequestId": "3dba7bcc-ea38-4371-b979-710fe3d0063d", "RetryAttempts": 0 }, "modelId": "afd_demo_model_20210512", "modelType": "ONLINE_FRAUD_INSIGHTS", "modelVersionNumber": "1.0", "status": "TRAINING_IN_PROGRESS" }, "text/plain": [ "" ] }, "metadata": { "application/json": { "expanded": false, "root": "root" } }, "output_type": "display_data" } ], "source": [ "\n", "response = client.create_model_version(\n", " modelId = MODEL_NAME,\n", " modelType = 'ONLINE_FRAUD_INSIGHTS',\n", " trainingDataSource = 'EXTERNAL_EVENTS',\n", " trainingDataSchema = trainingDataSchema,\n", " externalEventsDetail = {\n", " 'dataLocation' : S3_FILE_LOC,\n", " 'dataAccessRoleArn': DATA_ACCESS_ROLE_ARN\n", " }\n", ")\n", "\n", "# Display output\n", "display(HTML(\"

Model training

\"))\n", "display(JSON(response))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can check the progress of your model training by running the code cell below. Alternatively, you may also check the status of the training process via the Amazon Fraud Detector Console." ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2021-05-13 22:21:04.317898: Model status : TRAINING_COMPLETE\n", "\n", "Model training complete\n", "\n", "Elapsed time : 0.24937129020690918 seconds \n", "\n" ] }, { "data": { "application/json": { "ResponseMetadata": { "HTTPHeaders": { "connection": "keep-alive", "content-length": "791", "content-type": "application/x-amz-json-1.1", "date": "Thu, 13 May 2021 22:21:04 GMT", "x-amzn-requestid": "43dcf3c9-fc4b-42f9-8335-966ac1287339" }, "HTTPStatusCode": 200, "RequestId": "43dcf3c9-fc4b-42f9-8335-966ac1287339", "RetryAttempts": 0 }, "arn": "arn:aws:frauddetector:us-east-2:965425568475:model-version/ONLINE_FRAUD_INSIGHTS/afd_demo_model_20210512/1.0", "externalEventsDetail": { "dataAccessRoleArn": "arn:aws:iam::965425568475:role/service-role/AmazonFraudDetector-DataAccessRole-1620923454652", "dataLocation": "s3://sagemaker-us-east-2-965425568475/amazon-fraud-detector/training_data/afd_training_data.csv" }, "modelId": "afd_demo_model_20210512", "modelType": "ONLINE_FRAUD_INSIGHTS", "modelVersionNumber": "1.0", "status": "TRAINING_COMPLETE", "trainingDataSchema": { "labelSchema": { "labelMapper": { "FRAUD": [ "fraud" ], "LEGIT": [ "legit" ] } }, "modelVariables": [ "ip_address", "email_address", "user_agent", "customer_name", "phone_number", "customer_city", "customer_postal", "customer_state", "customer_address" ] }, "trainingDataSource": "EXTERNAL_EVENTS" }, "text/plain": [ "" ] }, "metadata": { "application/json": { "expanded": false, "root": "root" } }, "output_type": "display_data" } ], "source": [ "# Optional code cell\n", "from datetime import datetime\n", "\n", "print(\"Wait for model training to complete...\")\n", "stime = time.time()\n", "while True:\n", " current_time = datetime.now()\n", " clear_output(wait=True)\n", " response = client.get_model_version(modelId=MODEL_NAME, modelType = \"ONLINE_FRAUD_INSIGHTS\", modelVersionNumber = '1.0')\n", " if response['status'] == 'TRAINING_IN_PROGRESS':\n", " print(f\"{current_time}: current progress: {(time.time() - stime)/60:{3}.{3}} minutes\")\n", " time.sleep(60) # -- sleep for 60 seconds \n", " if response['status'] != 'TRAINING_IN_PROGRESS':\n", " print(f\"{current_time}: Model status : {response['status']}\")\n", " break\n", " \n", "etime = time.time()\n", "\n", "# -- summarize -- \n", "print(\"\\nModel training complete\")\n", "print(\"\\nElapsed time : %s\" % (etime - stime) + \" seconds \\n\" )\n", "display(JSON(response))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5. Check model performance and activate \n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once training is complete, the model version status willupdate to `TRAINING_COMPLETE`. You can review model performance using the Amazon Fraud Detector console or by calling [DescribeModelVersions](https://docs.aws.amazon.com/frauddetector/latest/api/API_DescribeModelVersions.html) API." ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [ { "data": { "image/png": "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uQU2fbvnuvVfBqetfhkUMC28FxGRJDJixOdcfvkUnIO77upM//4dfEeKWyph0eZcULpyV8Kb18OGBfvep+VpMOAtMA1EiohI8njmmU+4+urXALj33i7ccUdnz4nim0pYtH3zEvxn+J7LDY+EnMeCqUcADJp3VAETEZGk8sQTH/Gb3/wHgL///Wxuvvk0z4nin0pYtC2bHpzWPwyaHg85T0DdVn4ziYiIxFBRUYgJE4KZn8ce68H115/qOVFiUAmLJudg9QfB+d6vQtPj/OYRERGpBqmpKUybNohZs5Zx4YVH+46TMDQnFk1rP4WtyyGrcfCRQyIiIknKOce4cV9RUFAEQN26mSpgVaQSFk2LwpuvdhgKKTX3A0lFRCS5Oee44465DBw4kcsum+I7TsLSdGQ0bV4anLbUYkQREUlOzjluvnkmDz/8IampxgUXHOk7UsJSCYumbSuD0zqt/eYQERGJgVDIccMN/+GJJz4mPT2FceP60bevpiD3l0pYNOWGS1hdlTAREUkuoZDjV796jWef/ZSMjFQmTryY889v7ztWQlMJi5ZQIWxfDRjUPsh3GhERkah69NEPefbZT8nKSmPKlIF0736470gJTwvzo2XbanAhqN0cUtN9pxEREYmqq6/uyLnnHsHrrw9WAYsSjYRFyw+zg1NtTSEiIkmioKAI5yAjI5XatTN4/fXBmJnvWElDI2HRsmBUcHrUEL85REREomDXrkL693+FwYMnUlgYAlABizKNhEXD1uWw4i1Iy4b2/XynEREROSB5eYVcdNF4pk9fTMOGWSxbton27Rv7jpV0VMKi4fsZwelh50FmPb9ZREREDsCOHQX06fMys2Yto3HjbGbPHq4CFiMqYdGw6sPgtNUv/eYQERE5ANu25dOr11jeeut7mjWrzZw5wzn22Ga+YyUtlbBoWB0uYQf93G8OERGR/bR16y569hzN+++v4KCD6jB37iUcdVQT37GSmkpYNGz8BlIzodmJvpOIiIjsl7S0FDIyUmnduh5z5w6nXTtNQcaaSli0NDsJUjN8pxAREdkvtWqlM3XqIDZs2MEhhzTwHadG0BYV0XJoD98JREREqmTt2u38/vczKSgoAqBOnQwVsGqkkbBosFQ49grfKURERCK2enUu3bq9yIIF63AOHnqou+9INY5KWDQc0RvqtvKdQkREJCI//riVnJxR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GMDhw4epWLEi0dHRlCpViv379+eMbJy3f/9+qlSpAkDlypXZv39/gfe3Z88e6tevf9n1xsbG5lkeMGAAkydPZsiQIUyaNIkBAwYAsGvXLjIyMvIExKysLK/nnxcdHe0VVpKTk3n99dfZuXMnkB08cp9s4Plau3bt4s9//jOjRo3KWWetZe/evcTFxfHaa6/x/vvvs2/fPowxuFyuQj154fxopMvlyhldc7lcXuHyvNGjR5OWlkarVq0oW7Ysf/jDH1i3bp3X7YSSk5O58cYbqVu3bp59uVyuPNt57uv48eNUrFixUH420EiYiIiUUB07dmTYsGE5ZweGh4fTvn17pk2b5rXt1KlTSUxMBCApKYkFCxZw8uTJAu0nNjaWbdu25ftYeHh4nlGYAwcOeG3jeaHP3r17s2zZMlJTU/n0009zQlhsbCxly5bl8OHDOaNHLpeLTZs25bvvq6++mq1bt+Ys79q1iz/84Q+8+eab/P7776SlpdG8efM87TvPWmJjYxk3blyeEav09HRuuOEGli9fziuvvMLUqVM5evQoaWlpREVF5Xm93F566SWvsxNzf+UnOjqaGjVqsH79+pwsmL07AAAMd0lEQVR169evp1mzZvluHxYWxptvvsnevXvZvn07lStX5rrrriMkJCTPdsnJyXlGwQCaNWuWZz8nT55k27Ztefa1ZcuWPC3LK+ZrsligfmlivoiIc4rbxPxDhw7Z8uXL23Xr1llrrV2+fLktX768HTt2rHW5XPbIkSN29OjRNioqym7dutVaa+3p06dt69at7S233GK3bNliz507Zw8fPmxffPFF+8UXX3jt0+Vy2erVq9t//vOf9vTp09blctmVK1daa6199913baNGjezvv/9u9+/fb9u2bes1MT93ved17drVJiUl2VatWuVZ36NHDzt8+HB77Ngxe+7cOZuSkmKXLVuW7+/iwIEDtlKlSjY9Pd1aa+2mTZts2bJl7c8//2wzMzPthx9+aENCQux7771nrc2emN+hQ4c8rzFz5kzbrFkzu3HjRmtt9okB5ye+f/HFF7ZGjRp2//799syZM/bZZ5+1pUqVyvfnuRJPPPGEvemmm+yRI0fsli1bbPXq1e28efPy3TY1NdXu3bvXZmVl2e+++87Wrl07z6R+a6399ttvbfny5a3L5cqz/tChQzYyMtJOnz7dpqen27/8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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "\n", "auc = client.describe_model_versions(\n", " modelId= MODEL_NAME,\n", " modelVersionNumber='1.0',\n", " modelType='ONLINE_FRAUD_INSIGHTS',\n", " maxResults=10\n", ")['modelVersionDetails'][0]['trainingResult']['trainingMetrics']['auc']\n", "\n", "\n", "df_model = pd.DataFrame(client.describe_model_versions(\n", " modelId= MODEL_NAME,\n", " modelVersionNumber='1.0',\n", " modelType='ONLINE_FRAUD_INSIGHTS',\n", " maxResults=10\n", ")['modelVersionDetails'][0]['trainingResult']['trainingMetrics']['metricDataPoints'])\n", "\n", "\n", "plt.figure(figsize=(10,10))\n", "plt.plot(df_model[\"fpr\"], df_model[\"tpr\"], color='darkorange',\n", " lw=2, label='ROC curve (area = %0.3f)' % auc)\n", "plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')\n", "plt.xlabel('False Positive Rate')\n", "plt.ylabel('True Positive Rate')\n", "plt.title( MODEL_NAME + ' ROC Chart')\n", "plt.legend(loc=\"lower right\",fontsize=12)\n", "plt.axvline(x = 0.02 ,linewidth=2, color='r')\n", "plt.axhline(y = 0.73 ,linewidth=2, color='r')\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
fprprecisiontprthreshold
01.0000000.0504711.0000000.0
10.8852640.0563070.9937215.0
20.8135010.0606990.98901110.0
30.7607640.0645390.98744115.0
40.7190420.0680290.98744120.0
\n", "
" ], "text/plain": [ " fpr precision tpr threshold\n", "0 1.000000 0.050471 1.000000 0.0\n", "1 0.885264 0.056307 0.993721 5.0\n", "2 0.813501 0.060699 0.989011 10.0\n", "3 0.760764 0.064539 0.987441 15.0\n", "4 0.719042 0.068029 0.987441 20.0" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df_model.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Model Scores\n", "\n", "Amazon Fraud Detector generates model scores between 0 and 1000, where 0 is low fraud risk and1000 is high fraud risk. Model scores are directly related to the **false positive rate (FPR)**. For example, a score of 600 corresponds to an estimated 10% false positive rate whereas a score of 900 corresponds to an estimated 2% false positive rate. The following table provides details of how certain model scores correlate to estimated false positive rates.\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After reviewing the model performance, activate the model to make it available to use by Detectorsin real-time fraud predictions. Amazon Fraud Detector will deploy the model in multiple availability zones for redundancy with auto-scaling turned on to ensure the model scales with the number of fraud predictions you are making. To activate the model, call the UpdateModelVersionStatus API andupdate the status to `ACTIVE`." ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2021-05-14 04:13:34.844916Model status : ACTIVE\n", "\n", "Elapsed time : 602.9988882541656 seconds \n", "\n" ] }, { "data": { "application/json": { "ResponseMetadata": { "HTTPHeaders": { "connection": "keep-alive", "content-length": "780", "content-type": "application/x-amz-json-1.1", "date": "Fri, 14 May 2021 04:13:35 GMT", "x-amzn-requestid": "7b6a35b3-7347-473c-9b4f-1e41489d5966" }, "HTTPStatusCode": 200, "RequestId": "7b6a35b3-7347-473c-9b4f-1e41489d5966", "RetryAttempts": 0 }, "arn": "arn:aws:frauddetector:us-east-2:965425568475:model-version/ONLINE_FRAUD_INSIGHTS/afd_demo_model_20210512/1.0", "externalEventsDetail": { "dataAccessRoleArn": "arn:aws:iam::965425568475:role/service-role/AmazonFraudDetector-DataAccessRole-1620923454652", "dataLocation": "s3://sagemaker-us-east-2-965425568475/amazon-fraud-detector/training_data/afd_training_data.csv" }, "modelId": "afd_demo_model_20210512", "modelType": "ONLINE_FRAUD_INSIGHTS", "modelVersionNumber": "1.0", "status": "ACTIVE", "trainingDataSchema": { "labelSchema": { "labelMapper": { "FRAUD": [ "fraud" ], "LEGIT": [ "legit" ] } }, "modelVariables": [ "ip_address", "email_address", "user_agent", "customer_name", "phone_number", "customer_city", "customer_postal", "customer_state", "customer_address" ] }, "trainingDataSource": "EXTERNAL_EVENTS" }, "text/plain": [ "" ] }, "metadata": { "application/json": { "expanded": false, "root": "root" } }, "output_type": "display_data" } ], "source": [ "from datetime import datetime\n", "\n", "response = client.update_model_version_status (\n", " modelId = MODEL_NAME,\n", " modelType = 'ONLINE_FRAUD_INSIGHTS',\n", " modelVersionNumber = '1.0',\n", " status = 'ACTIVE'\n", ")\n", "print(\"Activating model...\")\n", "print(response)\n", "\n", "#-- wait until model is active \n", "print(\"Waiting until model status is active \")\n", "stime = time.time()\n", "while True:\n", " current_time = datetime.now()\n", " clear_output(wait=True)\n", " response = client.get_model_version(modelId=MODEL_NAME, modelType = \"ONLINE_FRAUD_INSIGHTS\", modelVersionNumber = '1.0')\n", " if response['status'] != 'ACTIVE':\n", " print(f\"{current_time}: current progress: {(time.time() - stime)/60:{3}.{3}} minutes\")\n", " time.sleep(60) # sleep for 1 minute \n", " if response['status'] == 'ACTIVE':\n", " print(f\"{current_time}Model status : {response['status']}\")\n", " break\n", " \n", "etime = time.time()\n", "print(\"\\nElapsed time : %s\" % (etime - stime) + \" seconds \\n\" )\n", "display(JSON(response))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 6. Create rules and detector \n", "-----\n", "overview" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A detector contains the detection logic, such as the models and rules, for a particular event that you want to evaluate for fraud. Each detector can evaluate one event type. A detector can have multiple versions, with each version having a status of `DRAFT`, `ACTIVE`, or `INACTIVE`. Only one detector version can be in ACTIVE status at a time.\n", "\n", "A detector acts as a container for your detector versions. The [PutDetector](https://docs.aws.amazon.com/frauddetector/latest/api/API_PutDetector.html) API specifies what event type the detector will evaluate." ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "data": { "application/json": { "ResponseMetadata": { "HTTPHeaders": { "connection": "keep-alive", "content-length": "2", "content-type": "application/x-amz-json-1.1", "date": "Fri, 14 May 2021 04:13:46 GMT", "x-amzn-requestid": "9df3c550-7506-48c4-b713-49fa746cbe13" }, "HTTPStatusCode": 200, "RequestId": "9df3c550-7506-48c4-b713-49fa746cbe13", "RetryAttempts": 0 } }, "text/plain": [ "" ] }, "metadata": { "application/json": { "expanded": false, "root": "root" } }, "output_type": "display_data" } ], "source": [ "\n", "response = client.put_detector(detectorId = DETECTOR_NAME, \n", " description = DETECTOR_DESC, \n", " eventTypeName = EVENT_TYPE )\n", "\n", "display(JSON(response))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Before we can create a detector version, we will need to create a set of rules. A rule is a condition that tells Amazon Fraud Detector how to interpret variable values during a fraud prediction. A rule consists of one or more variables, a logic expression, and one or more outcomes. A detector must have at least one associated rule. Rules in a detector are evaluated as part of a fraud prediction. We will start by defining outcomes first." ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [], "source": [ "outcome_list = [\n", " {\n", " \"name\": 'verify_customer',\n", " \"desc\": 'this outcome initiates a verification workflow'\n", " }, \n", " {\n", " \"name\": 'review',\n", " \"desc\": 'this outcome sidelines event for human or automated review'\n", " }, \n", " {\n", " \"name\": 'approve',\n", " \"desc\": 'this outcome approves the event'\n", " }\n", "]" ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Outcome verify_customer already exists ...\n", "Outcome review already exists ...\n", "Outcome approve already exists ...\n" ] } ], "source": [ "# Generate outcomes\n", "for outcome in outcome_list:\n", " try:\n", " client.get_outcomes(name = outcome['name'])\n", " print(f\"Outcome {outcome['name']} already exists ...\")\n", " except Exception as e:\n", " print(f\"Creating outcome: {outcome['name']} ...\")\n", " client.put_outcome(name = outcome['name'],\n", " description = outcome['desc'])\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have defined three possible outcomes -- `verify_customer`, `review`, and `approve` -- we will define the corresponding rules by setting up a calculation expression for each rule and then map each rule to each of the defined outcomes. The following code cell generates rules based on the false positive rate (FPR) score of the model. It considers fpr scores of between 1% and 6% only and defines the rules based on the threshold and maps a corresponding outcome to that rule. We will use output of this code cell to \n", "\n", "1. Create the individual rules using the [CreateRule](https://docs.aws.amazon.com/frauddetector/latest/api/API_CreateRule.html) API\n", "2. Generate a list of rules to be used by the detector version" ] }, { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Score thresholds 1% to 6% ...\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
fprtprthresholdruleoutcome
00.010.72970.0$afd_demo_model_20210512_insightscore > 970.0review
10.020.85920.0$afd_demo_model_20210512_insightscore > 920.0review
20.030.89880.0$afd_demo_model_20210512_insightscore > 880.0review
30.040.92835.0$afd_demo_model_20210512_insightscore > 835.0verify_customer
40.050.92795.0$afd_demo_model_20210512_insightscore > 795.0verify_customer
50.060.93750.0$afd_demo_model_20210512_insightscore <= 795.0approve
\n", "
" ], "text/plain": [ " fpr tpr threshold rule outcome\n", "0 0.01 0.72 970.0 $afd_demo_model_20210512_insightscore > 970.0 review\n", "1 0.02 0.85 920.0 $afd_demo_model_20210512_insightscore > 920.0 review\n", "2 0.03 0.89 880.0 $afd_demo_model_20210512_insightscore > 880.0 review\n", "3 0.04 0.92 835.0 $afd_demo_model_20210512_insightscore > 835.0 verify_customer\n", "4 0.05 0.92 795.0 $afd_demo_model_20210512_insightscore > 795.0 verify_customer\n", "5 0.06 0.93 750.0 $afd_demo_model_20210512_insightscore <= 795.0 approve" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "model_stat = df_model.round(decimals=2) \n", "\n", "m = model_stat.loc[model_stat.groupby([\"fpr\"])[\"threshold\"].idxmax()] \n", "\n", "def make_rule(x):\n", " rule = \"\"\n", " if x['fpr'] <= 0.05: \n", " rule = f\"${MODEL_NAME}_insightscore > {x['threshold']}\"\n", " if x['fpr'] == 0.06:\n", " rule = f\"${MODEL_NAME}_insightscore <= {x['threshold_prev']}\"\n", " return rule\n", " \n", "m[\"threshold_prev\"] = m['threshold'].shift(1)\n", "m['rule'] = m.apply(lambda x: make_rule(x), axis=1)\n", "\n", "m['outcome'] = \"approve\"\n", "m.loc[m['fpr'] <= 0.03, \"outcome\"] = \"review\"\n", "m.loc[(m['fpr'] > 0.03) & (m['fpr'] <= 0.05), \"outcome\"] = \"verify_customer\"\n", "\n", "print (\"Score thresholds 1% to 6% ...\")\n", "display(m[[\"fpr\", \"tpr\", \"threshold\", \"rule\", \"outcome\"]].loc[(m['fpr'] > 0.0 ) & (m['fpr'] <= 0.06)].reset_index(drop=True))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create rules using [CreateRule](https://docs.aws.amazon.com/frauddetector/latest/api/API_CreateRule.html) API and generate rule list" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import botocore\n", "\n", "rule_set = m[(m[\"fpr\"] > 0.0) & (m[\"fpr\"] <= 0.06)][[\"outcome\", \"rule\"]].to_dict('records')\n", "rule_list = []\n", "for i, rule in enumerate(rule_set):\n", " ruleId = \"rule{0}_{1}\".format(i, MODEL_NAME)\n", " rule_list.append({\"ruleId\": ruleId, \n", " \"ruleVersion\" : '1',\n", " \"detectorId\" : DETECTOR_NAME\n", " \n", " })\n", " \n", " try: \n", " response = client.create_rule(\n", " ruleId = ruleId,\n", " detectorId = DETECTOR_NAME,\n", " expression = rule['rule'],\n", " language = 'DETECTORPL',\n", " outcomes = [rule['outcome']]\n", " )\n", " print(f\"Creating rule: {ruleId}: IF {rule['rule']} THEN {rule['outcome']}\")\n", "# except client.exceptions.ValidationException as error:\n", " except botocore.exceptions.ClientError as error:\n", " print(f\"Rule {ruleId} already exists in this detector...\")\n", " print(error.response['Error']['Message'])\n", " except Exception as e:\n", " print(e)\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have created our rules, we will create a detector version using the detector we created earlier. A detector version defines the specific models and rules that will be run as part of the [GetEventPrediction](https://docs.aws.amazon.com/frauddetector/latest/api/API_GetEventPrediction.html) request. You can add any of the rules defined within a detector to the detector version. You can also add any model trained on the evaluated event type. Each detector version has a status of `DRAFT`, `ACTIVE`, or `INACTIVE`. Only one detector version can be in ACTIVE status at a time. During the GetEventPrediction request, Amazon Fraud Detector will use the `ACTIVE` detector if no `DetectorVersion` is specified.\n", "\n", "Amazon Fraud Detector supports two different rule execution modes: FIRST_MATCHED andALL_MATCHED.\n", "\n", "* If the rule execution mode is `FIRST_MATCHED`, Amazon Fraud Detector evaluates rules sequentially, first to last, stopping at the first matched rule. Amazon Fraud Detector then provides the outcomes for that single rule. If a rule evaluates to false (not matched), the next rule in the list is evaluated.\n", "* If the rule execution mode is `ALL_MATCHED`, then all rules in an evaluation are executed in parallel,regardless of their order. Amazon Fraud Detector executes all rules and returns the defined outcomes for every matched rule." ] }, { "cell_type": "code", "execution_count": 64, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Detector version created ... \n" ] }, { "data": { "application/json": { "ResponseMetadata": { "HTTPHeaders": { "connection": "keep-alive", "content-length": "79", "content-type": "application/x-amz-json-1.1", "date": "Tue, 18 May 2021 22:45:32 GMT", "x-amzn-requestid": "b78e492d-77cb-4ea9-a29f-4dd06a2cf7f2" }, "HTTPStatusCode": 200, "RequestId": "b78e492d-77cb-4ea9-a29f-4dd06a2cf7f2", "RetryAttempts": 0 }, "detectorId": "afd_detector_20210512", "detectorVersionId": "2", "status": "DRAFT" }, "text/plain": [ "" ] }, "metadata": { "application/json": { "expanded": false, "root": "root" } }, "output_type": "display_data" } ], "source": [ "\n", "response = client.create_detector_version(detectorId = DETECTOR_NAME,\n", " rules = rule_list,\n", " modelVersions = [\n", " {\n", " \"modelId\":MODEL_NAME, \n", " \"modelType\" : \"ONLINE_FRAUD_INSIGHTS\",\n", " \"modelVersionNumber\" : \"1.0\"\n", " }\n", " ],\n", " ruleExecutionMode = 'FIRST_MATCHED'\n", " )\n", "print(\"Detector version created ... \")\n", "display(JSON(response))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To update the status of a detector version, use the [UpdateDetectorVersionStatus](https://docs.aws.amazon.com/frauddetector/latest/api/API_UpdateDetectorVersionStatus.html) API." ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Detector activated ... \n" ] }, { "data": { "application/json": { "ResponseMetadata": { "HTTPHeaders": { "connection": "keep-alive", "content-length": "2", "content-type": "application/x-amz-json-1.1", "date": "Tue, 18 May 2021 22:58:39 GMT", "x-amzn-requestid": "14caafa4-92fc-422c-9554-b2bcb93f1d6f" }, "HTTPStatusCode": 200, "RequestId": "14caafa4-92fc-422c-9554-b2bcb93f1d6f", "RetryAttempts": 0 } }, "text/plain": [ "" ] }, "metadata": { "application/json": { "expanded": false, "root": "root" } }, "output_type": "display_data" } ], "source": [ "\n", "response = client.update_detector_version_status(detectorId= DETECTOR_NAME,\n", " detectorVersionId='1',\n", " status='ACTIVE'\n", " )\n", "print(\"Detector activated ... \")\n", "display(JSON(response))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 7. Make Predictions \n", "-----\n", "overview\n", "\n", "Our Amazon Fraud Detector model is now ready to make predictions. There are two modes you can run inference on Amazon Fraud Detector model to get predictions -\n", "\n", "1. Real-time Prediction\n", "2. Batch Prediction\n", "\n", "The following section will apply your detector to the first 10 records in your training dataset in real-time prediction mode. Ideally, real-time prediction is suitable for applications that need real-time evaluation of fraud for example web or mobile app signup page. A common architecture of a real-time fraud detection service would be to utilize the `GetEventPrediction` API from a [Lambda function](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html) which gets real-time fraud detection request from a web/mobile app via an [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) endpoint.\n", "\n", "#### 7.1 Real-time predictions\n", "---\n", "The code below loops through the first 10 records in our training data and runs predictions on them." ] }, { "cell_type": "code", "execution_count": 61, "metadata": {}, "outputs": [], "source": [ "# -- this will apply your detector to the first 10 records of your trainig dataset. -- \n", "record_count = 10 \n", "predicted_dat = []\n", "dateTimeObj = datetime.now()\n", "timestampStr = dateTimeObj.strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n", "pred_data = df[eventVariables].head(record_count).astype(str).to_dict(orient='records')\n", "\n", "for rec in pred_data:\n", " eventId = uuid.uuid1()\n", " pred = client.get_event_prediction(detectorId=DETECTOR_NAME, \n", " detectorVersionId='1',\n", " eventId = str(eventId),\n", " eventTypeName = EVENT_TYPE,\n", " eventTimestamp = timestampStr, \n", " entities = [\n", " {\n", " 'entityType': ENTITY_TYPE, \n", " 'entityId':str(eventId.int)\n", " }\n", " ],\n", " eventVariables=rec) \n", " \n", " rec[\"score\"] = pred['modelScores'][0]['scores'][f\"{MODEL_NAME}_insightscore\"]\n", " rec[\"outcome\"] = pred['ruleResults'][0]['outcomes']\n", " predicted_dat.append(rec)" ] }, { "cell_type": "code", "execution_count": 62, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
ip_addressemail_addressuser_agentcustomer_namephone_numbercustomer_citycustomer_postalcustomer_statecustomer_addressscoreoutcome
0168.49.13.67synth_emily31@gmail.comOpera/8.80.(Windows NT 5.0; ia-FR) Presto/2.9....Tabitha Rodgers(555)416 - 4931West Brenda33426.0WA8252 Garcia Pass Suite 14838.0[approve]
1212.201.60.16synth_kjohnson@hotmail.comMozilla/5.0 (iPad; CPU iPad OS 9_3_5 like Mac ...Jeremy Scott(555)514 - 7683New Sethberg32170.0ND201 Bowen Harbor43.0[approve]
2116.14.61.213synth_swansonchristopher@gmail.comMozilla/5.0 (Android 4.0; Mobile; rv:27.0) Gec...Whitney Cabrera(555)858 - 6712Jacksonborough32994.0CO524 Burke Inlet281.0[approve]
3192.89.123.199synth_ufox@yahoo.comMozilla/5.0 (iPad; CPU iPad OS 5_1_1 like Mac ...Timothy Campos(555)349 - 6656Christophershire34828.0AK41584 Kelli Mount26.0[approve]
4192.88.215.76synth_sbrewer@yahoo.comOpera/9.75.(X11; Linux i686; iu-CA) Presto/2.9...Justin Mathews(555)519 - 3973Richardside32519.0OR6807 Joseph Hill31.0[approve]
5203.0.18.22synth_sparkstimothy@yahoo.comMozilla/5.0 (compatible; MSIE 6.0; Windows NT ...John Bright(555)620 - 4924Lake Carol32851.0NY0226 David Groves Suite 95833.0[approve]
6198.51.97.207synth_justin87@hotmail.comMozilla/5.0 (iPad; CPU iPad OS 6_1_6 like Mac ...Crystal Williams(555)270 - 3596Brandonbury34476.0WA5130 Brian Row35.0[approve]
7192.30.212.77synth_pattonjames@gmail.comOpera/9.90.(X11; Linux i686; ak-GH) Presto/2.9...Robert Berry(555)817 - 8201Keithchester33288.0OR79636 Tricia Cape Suite 13549.0[approve]
8203.7.244.65synth_debbie01@hotmail.comMozilla/5.0 (iPad; CPU iPad OS 10_3_3 like Mac...Douglas Lambert MD(555)320 - 5839East Paigehaven32006.0WA44509 Hall Ford Suite 19827.0[approve]
9198.51.58.6synth_chelseaboyer@gmail.comOpera/9.53.(Windows 98; tr-CY) Presto/2.9.162 ...David Hall(555)248 - 9638Lake Brandonchester32782.0CT1149 Jones Spurs Suite 787407.0[approve]
\n", "
" ], "text/plain": [ " ip_address email_address user_agent customer_name phone_number customer_city customer_postal customer_state customer_address score outcome\n", "0 168.49.13.67 synth_emily31@gmail.com Opera/8.80.(Windows NT 5.0; ia-FR) Presto/2.9.... Tabitha Rodgers (555)416 - 4931 West Brenda 33426.0 WA 8252 Garcia Pass Suite 148 38.0 [approve]\n", "1 212.201.60.16 synth_kjohnson@hotmail.com Mozilla/5.0 (iPad; CPU iPad OS 9_3_5 like Mac ... Jeremy Scott (555)514 - 7683 New Sethberg 32170.0 ND 201 Bowen Harbor 43.0 [approve]\n", "2 116.14.61.213 synth_swansonchristopher@gmail.com Mozilla/5.0 (Android 4.0; Mobile; rv:27.0) Gec... Whitney Cabrera (555)858 - 6712 Jacksonborough 32994.0 CO 524 Burke Inlet 281.0 [approve]\n", "3 192.89.123.199 synth_ufox@yahoo.com Mozilla/5.0 (iPad; CPU iPad OS 5_1_1 like Mac ... Timothy Campos (555)349 - 6656 Christophershire 34828.0 AK 41584 Kelli Mount 26.0 [approve]\n", "4 192.88.215.76 synth_sbrewer@yahoo.com Opera/9.75.(X11; Linux i686; iu-CA) Presto/2.9... Justin Mathews (555)519 - 3973 Richardside 32519.0 OR 6807 Joseph Hill 31.0 [approve]\n", "5 203.0.18.22 synth_sparkstimothy@yahoo.com Mozilla/5.0 (compatible; MSIE 6.0; Windows NT ... John Bright (555)620 - 4924 Lake Carol 32851.0 NY 0226 David Groves Suite 958 33.0 [approve]\n", "6 198.51.97.207 synth_justin87@hotmail.com Mozilla/5.0 (iPad; CPU iPad OS 6_1_6 like Mac ... Crystal Williams (555)270 - 3596 Brandonbury 34476.0 WA 5130 Brian Row 35.0 [approve]\n", "7 192.30.212.77 synth_pattonjames@gmail.com Opera/9.90.(X11; Linux i686; ak-GH) Presto/2.9... Robert Berry (555)817 - 8201 Keithchester 33288.0 OR 79636 Tricia Cape Suite 135 49.0 [approve]\n", "8 203.7.244.65 synth_debbie01@hotmail.com Mozilla/5.0 (iPad; CPU iPad OS 10_3_3 like Mac... Douglas Lambert MD (555)320 - 5839 East Paigehaven 32006.0 WA 44509 Hall Ford Suite 198 27.0 [approve]\n", "9 198.51.58.6 synth_chelseaboyer@gmail.com Opera/9.53.(Windows 98; tr-CY) Presto/2.9.162 ... David Hall (555)248 - 9638 Lake Brandonchester 32782.0 CT 1149 Jones Spurs Suite 787 407.0 [approve]" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "predictions = pd.DataFrame(predicted_dat)\n", "display(predictions)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 7.2 Batch Predictions\n", "---\n", "You may also perform batch predictions via the [CreateBatchPredictionJob](https://docs.aws.amazon.com/frauddetector/latest/api/API_CreateBatchPredictionJob.html) API. With this method you may pass a file with input data, located in an S3 bucket specified by the `inputPath` parameter, to the prediction job. The prediction output will be eventually written into the S3 location specified in the `outputPath` parameter." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import s3fs\n", "\n", "%store -r afd_bucket\n", "%store -r afd_prefix\n", "\n", "df = pd.read_csv(S3_FILE_LOC)\n", "\n", "PREDICTION_OUTPUT_PATH=f\"s3://{afd_bucket}/{afd_prefix}/batch_prediction/outcomes.csv\"\n", "\n", "# Note you must prepare an input csv file without the EVENT_LABEL and EVENT_TIMESTAMP columns for example df[eventVariables]\n", "# this file is not provided in this tutorial but please feel free to create one using the \"df\" dataframe \n", "PREDICTION_INPUT_PATH=f\"s3://{afd_bucket}/{afd_prefix}/batch_prediction/input.csv\" \n", "\n", "JOB_ID=f'{MODEL_NAME}_{sufx}'\n", "\n", "response = client.create_batch_prediction_job(jobId=JOB_ID,\n", " inputPath=S3_FILE_LOC,\n", " outputPath=PREDICTION_OUTPUT_PATH,\n", " eventTypeName=EVENT_TYPE,\n", " detectorName=DETECTOR_NAME,\n", " detectorVersion='1',\n", " iamRoleArn=ARN_ROLE)\n", "print(\"Batch Prediction job created ... \")\n", "display(JSON(response))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Wait for Batch prediction job to complete" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"Waiting until job status is complete \")\n", "stime = time.time()\n", "while True:\n", " current_time = datetime.now()\n", " clear_output(wait=True)\n", " response = client.get_batch_prediction_jobs(jobId=JOB_ID,\n", " maxResults=1)\n", " status = response['batchPredictions'][0]['status']\n", " if status != 'COMPLETE':\n", " print(f\"{current_time}: current progress: {(time.time() - stime)/60:{3}.{3}} minutes\")\n", " time.sleep(60) # sleep for 1 minute \n", " if response['status'] == 'COMPLETE':\n", " print(f\"{current_time} Batch Prediction Job status : {status}\")\n", " break\n", " \n", "etime = time.time()\n", "print(\"\\nElapsed time : %s\" % (etime - stime) + \" seconds \\n\" )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Load and analyze predictions" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_pred = pd.read_csv(PREDICTION_OUTPUT_PATH)\n", "df_pred.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 8. Conclusion \n", "-----\n", "overview\n", "\n", "In this notebook \n", "\n", "* We created and trained our Amazon Fraud detector model\n", "* We analyzed the model's performance by looking at the False Positive Rate\n", "* We created rules, detector and detector version\n", "* Did real-time and batch predictions using our trained Amazon Fraud Detector model" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "instance_type": "ml.t3.medium", "kernelspec": { "display_name": "Python 3 (Data Science)", "language": "python", "name": "python3__SAGEMAKER_INTERNAL__arn:aws:sagemaker:us-east-2:429704687514:image/datascience-1.0" }, "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.7.10" } }, "nbformat": 4, "nbformat_minor": 4 }