{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Credit card fraud detector using Amazon SageMaker" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Investigate and process the data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's start by downloading the dataset from: https://www.kaggle.com/mlg-ulb/creditcardfraud?select=creditcard.csv and upload it into the notebook file system as creditcard.csv." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np \n", "import pandas as pd\n", "\n", "data = pd.read_csv('creditcard.csv', delimiter=',')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's take a peek at our data (we only show a subset of the columns in the table):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(data.columns)\n", "data[['Time', 'V1', 'V2', 'V27', 'V28', 'Amount', 'Class']].describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The class column corresponds to whether or not a transaction is fradulent. We see that the majority of data is non-fraudulent with only $492$ ($.173\\%$) of the data corresponding to fraudulent examples." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "nonfrauds, frauds = data.groupby('Class').size()\n", "print('Number of frauds: ', frauds)\n", "print('Number of non-frauds: ', nonfrauds)\n", "print('Percentage of fradulent data:', 100.*frauds/(frauds + nonfrauds))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This dataset has 28 columns, $V_i$ for $i=1..28$ of anonymized features along with columns for time, amount, and class. We already know that the columns $V_i$ have been normalized to have $0$ mean and unit standard deviation as the result of a PCA." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "feature_columns = data.columns[:-1]\n", "label_column = data.columns[-1]\n", "\n", "features = data[feature_columns].values.astype('float32')\n", "labels = (data[label_column].values).astype('float32')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we will prepare our data for loading and training." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Training" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We will split our dataset into a train and test to evaluate the performance of our models. It's important to do so _before_ any techniques meant to alleviate the class imbalance are used. This ensures that we don't leak information from the test set into the train set." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import train_test_split\n", "\n", "X_train, X_test, y_train, y_test = train_test_split(\n", " features, labels, test_size=0.1, random_state=42)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Unsupervised Learning" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In a fraud detection scenario, commonly we will have very few labeled examples, and it's possible that labeling fraud takes a very long time. We would like then to extract information from the unlabeled data we have at hand as well. _Anomaly detection_ is a form of unsupervised learning where we try to identify anomalous examples based solely on their feature characteristics. Random Cut Forest is a state-of-the-art anomaly detection algorithm that is both accurate and scalable. We will train such a model on our training data and evaluate its performance on our test set." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import boto3\n", "import os\n", "import sagemaker\n", "from sagemaker import get_execution_role\n", "\n", "session = sagemaker.Session()\n", "bucket = \"renbrave-fraud-detection-model\"\n", "prefix = 'fraud-classifier'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker import RandomCutForest\n", "\n", "# specify general training job information\n", "rcf = RandomCutForest(role=get_execution_role(),\n", " train_instance_count=1,\n", " train_instance_type='ml.c4.xlarge',\n", " data_location='s3://{}/{}/'.format(bucket, prefix),\n", " output_path='s3://{}/{}/output'.format(bucket, prefix),\n", " num_samples_per_tree=512,\n", " num_trees=50)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rcf.fit(rcf.record_set(X_train))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Host Random Cut Forest" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once we have a trained model we can deploy it and get some predictions for our test set." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rcf_predictor = rcf.deploy(\n", " endpoint_name='random-cut-forest-endpoint',\n", " initial_instance_count=1,\n", " instance_type='ml.c4.xlarge')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.predictor import csv_serializer, json_deserializer\n", "\n", "rcf_predictor.content_type = 'text/csv'\n", "rcf_predictor.serializer = csv_serializer\n", "rcf_predictor.accept = 'application/json'\n", "rcf_predictor.deserializer = json_deserializer" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Test Random Cut Forest" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With the model deployed, let's see how it performs in terms of separating fraudulent from legitimate transactions." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def predict_rcf(current_predictor, data, rows=500):\n", " split_array = np.array_split(data, int(data.shape[0] / float(rows) + 1))\n", " predictions = []\n", " for array in split_array:\n", " array_preds = [s['score'] for s in current_predictor.predict(array)['scores']]\n", " predictions.append(array_preds)\n", "\n", " return np.concatenate([np.array(batch) for batch in predictions])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "positives = X_test[y_test == 1]\n", "positives_scores = predict_rcf(rcf_predictor, positives)\n", "\n", "negatives = X_test[y_test == 0]\n", "negatives_scores = predict_rcf(rcf_predictor, negatives)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import seaborn as sns\n", "import matplotlib.pyplot as plt\n", "sns.set(color_codes=True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sns.distplot(positives_scores, label='fraud', bins=20)\n", "sns.distplot(negatives_scores, label='not-fraud', bins=20)\n", "plt.legend()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The unsupervised model already can achieve some separation between the classes, with higher anomaly scores being correlated to fraud." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Supervised Learning" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once we have gathered an adequate amount of labeled training data, we can use a supervised learning algorithm that discovers relationships between the features and the dependent class.\n", "\n", "We will use Gradient Boosted Trees as our model, as they have a proven track record, are highly scalable and can deal with missing data, reducing the need to pre-process datasets." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Prepare Data and Upload to S3" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First we copy the data to an in-memory buffer." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'X_train' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mbuf\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mio\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mBytesIO\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 7\u001b[0;31m \u001b[0msklearn\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatasets\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdump_svmlight_file\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mX_train\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my_train\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbuf\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 8\u001b[0m \u001b[0mbuf\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mseek\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m;\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mNameError\u001b[0m: name 'X_train' is not defined" ] } ], "source": [ "import io\n", "import sklearn\n", "from sklearn.datasets import dump_svmlight_file \n", "\n", "buf = io.BytesIO()\n", "\n", "sklearn.datasets.dump_svmlight_file(X_train, y_train, buf)\n", "buf.seek(0);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we upload the data to S3 using boto3." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "key = 'fraud-dataset'\n", "subdir = 'base'\n", "boto3.resource('s3').Bucket(bucket).Object(os.path.join(prefix, 'train', subdir, key)).upload_fileobj(buf)\n", "\n", "s3_train_data = 's3://{}/{}/train/{}/{}'.format(bucket, prefix, subdir, key)\n", "print('Uploaded training data location: {}'.format(s3_train_data))\n", "\n", "output_location = 's3://{}/{}/output'.format(bucket, prefix)\n", "print('Training artifacts will be uploaded to: {}'.format(output_location))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now train using SageMaker's built-in XGBoost algorithm. To specify the XGBoost algorithm, we use a utility function to obtain its URI. A complete list of built-in algorithms is found here: https://docs.aws.amazon.com/sagemaker/latest/dg/algos.html" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.amazon.amazon_estimator import get_image_uri\n", "\n", "container = get_image_uri(boto3.Session().region_name, 'xgboost', repo_version='0.90-2')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "SageMaker abstracts training via Estimators. We can pass the classifier and parameters along with hyperparameters to the estimator, and fit the estimator to the data in S3. An important parameter here is `scale_pos_weight` which scales the weights of the positive vs. negative class examples. This is crucial to do in an imbalanced dataset like the one we are using here, otherwise the majority class would dominate the learning." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "from math import sqrt\n", "from sagemaker import get_execution_role\n", "\n", "# Because the data set is so highly skewed, we set the scale position weight conservatively,\n", "# as sqrt(num_nonfraud/num_fraud).\n", "# Other recommendations for the scale_pos_weight are setting it to (num_nonfraud/num_fraud).\n", "scale_pos_weight = sqrt(np.count_nonzero(y_train==0)/np.count_nonzero(y_train))\n", "hyperparams = {\n", " \"max_depth\":5,\n", " \"subsample\":0.8,\n", " \"num_round\":100,\n", " \"eta\":0.2,\n", " \"gamma\":4,\n", " \"min_child_weight\":6,\n", " \"silent\":0,\n", " \"objective\":'binary:logistic',\n", " \"eval_metric\":'auc',\n", " \"scale_pos_weight\": scale_pos_weight\n", "}\n", "\n", "clf = sagemaker.estimator.Estimator(container,\n", " get_execution_role(),\n", " hyperparameters=hyperparams,\n", " train_instance_count=1, \n", " train_instance_type='ml.m4.xlarge',\n", " output_path=output_location,\n", " sagemaker_session=session)\n", "clf.fit({'train': s3_train_data})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Host Classifier" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we deploy the estimator to and endpoint." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sagemaker.predictor import csv_serializer\n", "\n", "predictor = clf.deploy(initial_instance_count=1,\n", " endpoint_name=\"fraud-detection-endpoint\",\n", " instance_type='ml.m4.xlarge', \n", " serializer=csv_serializer,\n", " deserializer=None,\n", " content_type='text/csv')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Evaluation" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once we have trained the model we can use it to make predictions for the test set." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Because we have a large test set, we call predict on smaller batches\n", "def predict(current_predictor, data, rows=500):\n", " split_array = np.array_split(data, int(data.shape[0] / float(rows) + 1))\n", " predictions = ''\n", " for array in split_array:\n", " predictions = ','.join([predictions, current_predictor.predict(array).decode('utf-8')])\n", "\n", " return np.fromstring(predictions[1:], sep=',')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "raw_preds = predict(predictor, X_test)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We will use a few measures from the scikit-learn package to evaluate the performance of our model. When dealing with an imbalanced dataset, we need to choose metrics that take into account the frequency of each class in the data.\n", "\n", "Two such metrics are the [balanced accuracy score](https://scikit-learn.org/stable/modules/model_evaluation.html#balanced-accuracy-score), and [Cohen's Kappa](https://scikit-learn.org/stable/modules/model_evaluation.html#cohen-s-kappa)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.metrics import balanced_accuracy_score, cohen_kappa_score\n", "\n", "# scikit-learn expects 0/1 predictions, so we threshold our raw predictions\n", "y_preds = np.where(raw_preds > 0.5, 1, 0)\n", "print(\"Balanced accuracy = {}\".format(balanced_accuracy_score(y_test, y_preds)))\n", "print(\"Cohen's Kappa = {}\".format(cohen_kappa_score(y_test, y_preds)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can already see that our model performs very well in terms of both metrics, Cohen's Kappa scores above 0.8 are generally very favorable." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Apart from single-value metrics, it's also useful to look at metrics that indicate performance per class. A confusion matrix, and per-class precision, recall and f1-score can also provide more information about the model's performance." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "from sklearn.metrics import confusion_matrix\n", "\n", "def plot_confusion_matrix(y_true, y_predicted):\n", "\n", " cm = confusion_matrix(y_true, y_predicted)\n", " # Get the per-class normalized value for each cell\n", " cm_norm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]\n", " \n", " # We color each cell according to its normalized value, annotate with exact counts.\n", " ax = sns.heatmap(cm_norm, annot=cm, fmt=\"d\")\n", " ax.set(xticklabels=[\"non-fraud\", \"fraud\"], yticklabels=[\"non-fraud\", \"fraud\"])\n", " ax.set_ylim([0,2])\n", " plt.title('Confusion Matrix')\n", " plt.ylabel('Real Classes')\n", " plt.xlabel('Predicted Classes')\n", " plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_confusion_matrix(y_test, y_preds)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.metrics import classification_report\n", "\n", "print(classification_report(\n", " y_test, y_preds, target_names=['non-fraud', 'fraud']))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Keep sending test traffic to the endpoint via lambda" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now show how we could use both of these models in a production system, using HTTP requests to an AWS Lambda function that invokes both the unsupervised and the supervised SageMaker endpoints.\n", "\n", "We create a background thread that will constantly create HTTP requests to invoke the Lambda, using our test data as input. See the included `generate_endpoint_traffic.py` file to see how that is done. The output will be logged to Kinesis, and you can also observe it in the Lambda function's CloudWatch logs." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from threading import Thread\n", "from generate_endpoint_traffic import generate_traffic\n", "\n", "thread = Thread(target = generate_traffic, args=[np.copy(X_test)])\n", "thread.start()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### SMOTE" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have a baseline model using XGBoost, we can try to see if sampling techniques that are designed specifically for imbalanced problems can improve the performance of the model.\n", "\n", "For that purpose we will be using the [imbalanced-learn](https://imbalanced-learn.readthedocs.io/en/stable/index.html) package that works well with scikit-learn. We have pre-installed the package for this kernel, but if you need it for a different Jupyter kernel you can install it by running `pip install --upgrade imbalanced-learn` within the conda environment you need.\n", "\n", "We will be using [Sythetic Minority Over-sampling](https://arxiv.org/abs/1106.1813) (SMOTE), which oversamples the minority class by interpolating new data points between existing ones." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from imblearn.over_sampling import SMOTE\n", "\n", "smote = SMOTE(random_state=42)\n", "X_smote, y_smote = smote.fit_resample(X_train, y_train)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that SMOTE has now balanced the two classes:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from collections import Counter\n", "print(sorted(Counter(y_smote).items()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We note that this is a case of extreme oversampling of the the minority class, we went from ~0.17% to 50%. An alternative would be to use a smaller resampling ratio, such as having one minority cl\n", "ass sample for every `sqrt(non_fraud/fraud)` majority samples, or using more advanced resampling techniques. See the [comparison](https://imbalanced-learn.readthedocs.io/en/stable/auto_examples/over-sampling/plot_comparison_over_sampling.html#sphx-glr-auto-examples-over-sampling-plot-comparison-over-sampling-py) provided by imbalanced-learn for more over-sampling options." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In our case we'll use the SMOTE dataset we just created and upload it to S3 for training." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smote_buf = io.BytesIO()\n", "\n", "# Dump the SMOTE data into a buffer\n", "sklearn.datasets.dump_svmlight_file(X_smote, y_smote, smote_buf)\n", "smote_buf.seek(0);\n", "\n", "# Upload from the buffer to S3\n", "key = 'fraud-dataset-smote'\n", "subdir = 'smote'\n", "boto3.resource('s3').Bucket(bucket).Object(os.path.join(prefix, 'train', subdir, key)).upload_fileobj(smote_buf)\n", "\n", "s3_smote_train_data = 's3://{}/{}/train/{}/{}'.format(bucket, prefix, subdir, key)\n", "print('Uploaded training data location: {}'.format(s3_smote_train_data))\n", "\n", "smote_output_location = 's3://{}/{}/smote-output'.format(bucket, prefix)\n", "print('Training artifacts will be uploaded to: {}'.format(smote_output_location))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "try:\n", " hyperparams.pop(\"scale_pos_weight\") # No need to scale weights after SMOTE resampling, so we remove that parameter\n", "except KeyError:\n", " # In case the parameter was already removed, do nothing.\n", " pass\n", "smote_xgb = sagemaker.estimator.Estimator(container,\n", " get_execution_role(),\n", " hyperparameters=hyperparams,\n", " train_instance_count=1, \n", " train_instance_type='ml.m4.xlarge',\n", " output_path=smote_output_location,\n", " sagemaker_session=session)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smote_xgb.fit({'train': s3_smote_train_data})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After fitting the model we can check its performance to compare it against the base XGBoost model." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smote_predictor = smote_xgb.deploy(initial_instance_count=1,\n", " endpoint_name=\"fraud-detection-smote-endpoint\",\n", " instance_type='ml.m4.xlarge')\n", "\n", "# Specify input and output formats.\n", "smote_predictor.content_type = 'text/csv'\n", "smote_predictor.serializer = csv_serializer\n", "smote_predictor.deserializer = None" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smote_raw_preds = predict(smote_predictor, X_test)\n", "smote_preds = np.where(smote_raw_preds > 0.5, 1, 0)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"Balanced accuracy = {}\".format(balanced_accuracy_score(y_test, smote_preds)))\n", "print(\"Cohen's Kappa = {}\".format(cohen_kappa_score(y_test, smote_preds)))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_confusion_matrix(y_test, smote_preds)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(classification_report(\n", " y_test, smote_preds, target_names=['non-fraud', 'fraud']))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Due to the randomness of XGBoost your results may vary, but overall, you should see a large increase in non-fraud cases being classified as fraud (false positives). The reason this happens is because SMOTE has oversampled the fraud class so much that it's increased its overlap in feature space with the non-fraud cases.\n", "Since Cohen's Kappa gives more weight to false positives than balanced accuracy does, the metric drops significantly, as does the precision and F1 score for fraud cases. However, we can bring a balance between the metrics again by adjusting our classification threshold." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So far we've been using 0.5 as the threshold between labeling a point as fraud or not. We can try different thresholds to see if they affect the result of the classification. To evaluate we'll use the balanced accuracy and Cohen's Kappa metrics." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for thres in np.linspace(0.1, 0.9, num=9):\n", " smote_thres_preds = np.where(smote_raw_preds > thres, 1, 0)\n", " print(\"Threshold: {:.1f}\".format(thres))\n", " print(\"Balanced accuracy = {:.3f}\".format(balanced_accuracy_score(y_test, smote_thres_preds)))\n", " print(\"Cohen's Kappa = {:.3f}\\n\".format(cohen_kappa_score(y_test, smote_thres_preds)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that Cohen's Kappa keeps increasing along with the threshold, without a significant loss in balanced accuracy. This adds a useful knob to our model: We can keep a low threshold if we care more about not missing any fraudulent cases, or we can increase the threshold to try to minimize the number of false positives." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Clean up\n", "\n", "We will leave the unsupervised and base XGBoost endpoints running at the end of this notebook so we can handle incoming event streams. However, don't forget to delete the prediction endpoints when you're done. You can do that at the Amazon SageMaker console in the Endpoints page. Or you can run `predictor_name.delete_endpoint()`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Uncomment to clean up endpoints\n", "# rcf_predictor.delete_endpoint()\n", "# predictor.delete_endpoint()\n", "smote_predictor.delete_endpoint()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "## Data Acknowledgements\n", "\n", "The dataset used to demonstrated the fraud detection solution has been collected and analysed during a research collaboration of Worldline and the Machine Learning Group (http://mlg.ulb.ac.be) of ULB (Université Libre de Bruxelles) on big data mining and fraud detection. More details on current and past projects on related topics are available on https://www.researchgate.net/project/Fraud-detection-5 and the page of the [DefeatFraud](https://mlg.ulb.ac.be/wordpress/portfolio_page/defeatfraud-assessment-and-validation-of-deep-feature-engineering-and-learning-solutions-for-fraud-detection/) project\n", "We cite the following works:\n", "* Andrea Dal Pozzolo, Olivier Caelen, Reid A. Johnson and Gianluca Bontempi. Calibrating Probability with Undersampling for Unbalanced Classification. In Symposium on Computational Intelligence and Data Mining (CIDM), IEEE, 2015\n", "* Dal Pozzolo, Andrea; Caelen, Olivier; Le Borgne, Yann-Ael; Waterschoot, Serge; Bontempi, Gianluca. Learned lessons in credit card fraud detection from a practitioner perspective, Expert systems with applications,41,10,4915-4928,2014, Pergamon\n", "* Dal Pozzolo, Andrea; Boracchi, Giacomo; Caelen, Olivier; Alippi, Cesare; Bontempi, Gianluca. Credit card fraud detection: a realistic modeling and a novel learning strategy, IEEE transactions on neural networks and learning systems,29,8,3784-3797,2018,IEEE\n", "* Dal Pozzolo, Andrea Adaptive Machine learning for credit card fraud detection ULB MLG PhD thesis (supervised by G. Bontempi)\n", "* Carcillo, Fabrizio; Dal Pozzolo, Andrea; Le Borgne, Yann-Aël; Caelen, Olivier; Mazzer, Yannis; Bontempi, Gianluca. Scarff: a scalable framework for streaming credit card fraud detection with Spark, Information fusion,41, 182-194,2018,Elsevier\n", "* Carcillo, Fabrizio; Le Borgne, Yann-Aël; Caelen, Olivier; Bontempi, Gianluca. Streaming active learning strategies for real-life credit card fraud detection: assessment and visualization, International Journal of Data Science and Analytics, 5,4,285-300,2018,Springer International Publishing" ] } ], "metadata": { "kernelspec": { "display_name": "conda_python3", "language": "python", "name": "conda_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.6.10" } }, "nbformat": 4, "nbformat_minor": 2 }