{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Online Retail - Predicting order cancellations\n",
"\n",
"In this tutorial, we demonstrate how getML can be applied in an e-commerce context. Using a dataset of about 400,000 orders, our goal is to predict whether an order will be cancelled.\n",
"\n",
"We also show that we can significantly improve our results by using getML's built-in hyperparameter tuning routines.\n",
"\n",
"Summary:\n",
"\n",
"- Prediction type: __Classification model__\n",
"- Domain: __E-commerce__\n",
"- Prediction target: __Whether an order will be cancelled__ \n",
"- Population size: __397925__"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Background\n",
"\n",
"The data set contains about 400,000 orders from a British online retailer. Each order consists of a product that has been ordered and a corresponding quantity. Several orders can be summarized onto a single invoice. The goal is to predict whether an order will be cancelled.\n",
"\n",
"Because the company mainly sells to other businesses, the cancellation rate is relatively low, namely 1.83%.\n",
"\n",
"The data set has been originally collected for this study:\n",
"\n",
"> Daqing Chen, Sai Liang Sain, and Kun Guo, Data mining for the online retail industry: A case study of RFM model-based customer segmentation using data mining, Journal of Database Marketing and Customer Strategy Management, Vol. 19, No. 3, pp. 197-208, 2012 (Published online before print: 27 August 2012. doi: 10.1057/dbm.2012.17).\n",
"\n",
"It has been downloaded from the UCI Machine Learning Repository:\n",
"\n",
"> Dua, D. and Graff, C. (2019). [UCI Machine Learning Repository](http://archive.ics.uci.edu/dataset/352/online+retail). Irvine, CA: University of California, School of Information and Computer Science."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Analysis"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's get started with the analysis and set up your session:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"%pip install -q \"getml==1.5.0\" \"pyspark==3.5.2\" \"ipywidgets==8.1.5\""
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"getML API version: 1.5.0\n",
"\n"
]
}
],
"source": [
"import os\n",
"from urllib import request\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"from pyspark.sql import SparkSession\n",
"import getml\n",
"\n",
"os.environ[\"PYARROW_IGNORE_TIMEZONE\"] = \"1\"\n",
"\n",
"print(f\"getML API version: {getml.__version__}\\n\")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Launching ./getML --allow-push-notifications=true --allow-remote-ips=true --home-directory=/home/user --in-memory=true --install=false --launch-browser=true --log=false --token=token in /home/user/.getML/getml-1.5.0-x64-linux...\n",
"Launched the getML Engine. The log output will be stored in /home/user/.getML/logs/20240912145332.log.\n",
"\u001b[2K Loading pipelines... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% • 00:00\n",
"\u001b[?25h"
]
},
{
"data": {
"text/html": [
"
Connected to project 'online_retail'.\n",
"
\n"
],
"text/plain": [
"Connected to project \u001b[32m'online_retail'\u001b[0m.\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"getml.engine.launch(allow_remote_ips=True, token='token')\n",
"getml.engine.set_project('online_retail')"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"RUN_SPARK = False"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 1. Loading data"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### 1.1 Download from source\n",
"\n",
"We begin by downloading the data from the source file:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"fname = \"online_retail.csv\"\n",
"\n",
"if not os.path.exists(fname):\n",
" fname, res = request.urlretrieve(\n",
" \"https://static.getml.com/datasets/online_retail/\" + fname, \n",
" fname\n",
" )\n",
" \n",
"full_data_pandas = pd.read_csv(fname, sep=\"|\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### 1.2 Data preparation\n",
"\n",
"The invoice dates are in a somewhat unusual format, fo we need to rectify that."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"def add_zero(string):\n",
" if len(string) == 1:\n",
" return \"0\" + string\n",
" return string"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"def format_date(string):\n",
" datetime = string.split(\" \")\n",
" assert len(datetime) == 2, \"Expected date and time\"\n",
" \n",
" date_components = datetime[0].split(\"/\")\n",
" assert len(date_components) == 3, \"Expected three date components\"\n",
" \n",
" date_components = [add_zero(x) for x in date_components]\n",
" \n",
" return \"-\".join(date_components) + \" \" + datetime[1] "
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"full_data_pandas[\"InvoiceDate\"] = [\n",
" format_date(string) for string in np.asarray(full_data_pandas[\"InvoiceDate\"])\n",
"]"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
"
\n",
"
\n",
"
Invoice
\n",
"
StockCode
\n",
"
Description
\n",
"
Quantity
\n",
"
InvoiceDate
\n",
"
Price
\n",
"
Customer ID
\n",
"
Country
\n",
"
\n",
" \n",
" \n",
"
\n",
"
0
\n",
"
536365
\n",
"
85123A
\n",
"
WHITE HANGING HEART T-LIGHT HOLDER
\n",
"
6
\n",
"
2010-12-01 08:26
\n",
"
2.55
\n",
"
17850.0
\n",
"
United Kingdom
\n",
"
\n",
"
\n",
"
1
\n",
"
536365
\n",
"
71053
\n",
"
WHITE METAL LANTERN
\n",
"
6
\n",
"
2010-12-01 08:26
\n",
"
3.39
\n",
"
17850.0
\n",
"
United Kingdom
\n",
"
\n",
"
\n",
"
2
\n",
"
536365
\n",
"
84406B
\n",
"
CREAM CUPID HEARTS COAT HANGER
\n",
"
8
\n",
"
2010-12-01 08:26
\n",
"
2.75
\n",
"
17850.0
\n",
"
United Kingdom
\n",
"
\n",
"
\n",
"
3
\n",
"
536365
\n",
"
84029G
\n",
"
KNITTED UNION FLAG HOT WATER BOTTLE
\n",
"
6
\n",
"
2010-12-01 08:26
\n",
"
3.39
\n",
"
17850.0
\n",
"
United Kingdom
\n",
"
\n",
"
\n",
"
4
\n",
"
536365
\n",
"
84029E
\n",
"
RED WOOLLY HOTTIE WHITE HEART.
\n",
"
6
\n",
"
2010-12-01 08:26
\n",
"
3.39
\n",
"
17850.0
\n",
"
United Kingdom
\n",
"
\n",
"
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
\n",
"
\n",
"
541905
\n",
"
581587
\n",
"
22899
\n",
"
CHILDREN'S APRON DOLLY GIRL
\n",
"
6
\n",
"
2011-12-09 12:50
\n",
"
2.10
\n",
"
12680.0
\n",
"
France
\n",
"
\n",
"
\n",
"
541906
\n",
"
581587
\n",
"
23254
\n",
"
CHILDRENS CUTLERY DOLLY GIRL
\n",
"
4
\n",
"
2011-12-09 12:50
\n",
"
4.15
\n",
"
12680.0
\n",
"
France
\n",
"
\n",
"
\n",
"
541907
\n",
"
581587
\n",
"
23255
\n",
"
CHILDRENS CUTLERY CIRCUS PARADE
\n",
"
4
\n",
"
2011-12-09 12:50
\n",
"
4.15
\n",
"
12680.0
\n",
"
France
\n",
"
\n",
"
\n",
"
541908
\n",
"
581587
\n",
"
22138
\n",
"
BAKING SET 9 PIECE RETROSPOT
\n",
"
3
\n",
"
2011-12-09 12:50
\n",
"
4.95
\n",
"
12680.0
\n",
"
France
\n",
"
\n",
"
\n",
"
541909
\n",
"
581587
\n",
"
POST
\n",
"
POSTAGE
\n",
"
1
\n",
"
2011-12-09 12:50
\n",
"
18.00
\n",
"
12680.0
\n",
"
France
\n",
"
\n",
" \n",
"
\n",
"
541910 rows × 8 columns
\n",
"
"
],
"text/plain": [
" Invoice StockCode Description Quantity \\\n",
"0 536365 85123A WHITE HANGING HEART T-LIGHT HOLDER 6 \n",
"1 536365 71053 WHITE METAL LANTERN 6 \n",
"2 536365 84406B CREAM CUPID HEARTS COAT HANGER 8 \n",
"3 536365 84029G KNITTED UNION FLAG HOT WATER BOTTLE 6 \n",
"4 536365 84029E RED WOOLLY HOTTIE WHITE HEART. 6 \n",
"... ... ... ... ... \n",
"541905 581587 22899 CHILDREN'S APRON DOLLY GIRL 6 \n",
"541906 581587 23254 CHILDRENS CUTLERY DOLLY GIRL 4 \n",
"541907 581587 23255 CHILDRENS CUTLERY CIRCUS PARADE 4 \n",
"541908 581587 22138 BAKING SET 9 PIECE RETROSPOT 3 \n",
"541909 581587 POST POSTAGE 1 \n",
"\n",
" InvoiceDate Price Customer ID Country \n",
"0 2010-12-01 08:26 2.55 17850.0 United Kingdom \n",
"1 2010-12-01 08:26 3.39 17850.0 United Kingdom \n",
"2 2010-12-01 08:26 2.75 17850.0 United Kingdom \n",
"3 2010-12-01 08:26 3.39 17850.0 United Kingdom \n",
"4 2010-12-01 08:26 3.39 17850.0 United Kingdom \n",
"... ... ... ... ... \n",
"541905 2011-12-09 12:50 2.10 12680.0 France \n",
"541906 2011-12-09 12:50 4.15 12680.0 France \n",
"541907 2011-12-09 12:50 4.15 12680.0 France \n",
"541908 2011-12-09 12:50 4.95 12680.0 France \n",
"541909 2011-12-09 12:50 18.00 12680.0 France \n",
"\n",
"[541910 rows x 8 columns]"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"full_data_pandas"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In this data set, the targets aren't as clearly defined as we would like to, so we have do define them ourselves."
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"def add_target(df):\n",
" df = df.sort_values(by=[\"Customer ID\", \"InvoiceDate\"])\n",
" \n",
" cancelled = np.zeros(df.shape[0])\n",
"\n",
" invoice = np.asarray(df[\"Invoice\"])\n",
" stock_code = np.asarray(df[\"StockCode\"])\n",
" customer_id = np.asarray(df[\"Customer ID\"])\n",
"\n",
" for i in range(len(invoice)):\n",
" if (invoice[i][0] == 'C') or (i == len(invoice) - 1):\n",
" continue\n",
"\n",
" j = i + 1\n",
"\n",
" while customer_id[j] == customer_id[i]:\n",
" if (invoice[j][0] == 'C') and (stock_code[i] == stock_code[j]):\n",
" cancelled[i] = 1.0\n",
" break\n",
"\n",
" if stock_code[i] == stock_code[j]:\n",
" break\n",
"\n",
" j += 1\n",
" \n",
" df[\"cancelled\"] = cancelled\n",
" \n",
" return df"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Also, we want to remove any orders in the data set that are actually cancellations."
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"def remove_cancellations(df):\n",
" invoice = np.asarray(df[\"Invoice\"])\n",
"\n",
" is_order = [inv[0] != 'C' for inv in invoice]\n",
" \n",
" df = df[is_order]\n",
" \n",
" return df"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"full_data_pandas = add_target(full_data_pandas)\n",
"full_data_pandas = remove_cancellations(full_data_pandas)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Finally, there are some order for which we do not have a customer ID. We want to remove those."
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"full_data_pandas = full_data_pandas[~np.isnan(full_data_pandas[\"Customer ID\"])]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we can upload the data to getML."
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n"
],
"text/plain": [
" name Quantity Price Customer ID cancelled Invoice StockCode \n",
" role unused_float unused_float unused_float unused_float unused_string unused_string\n",
" 0 74215 1.04 12346 1 541431 23166 \n",
" 1 12 2.1 12347 0 537626 85116 \n",
" 2 4 4.25 12347 0 537626 22375 \n",
" 3 12 3.25 12347 0 537626 71477 \n",
" 4 36 0.65 12347 0 537626 22492 \n",
" ... ... ... ... ... ... \n",
"397920 12 0.42 18287 0 570715 22419 \n",
"397921 12 2.1 18287 0 570715 22866 \n",
"397922 36 1.25 18287 0 573167 23264 \n",
"397923 48 0.39 18287 0 573167 21824 \n",
"397924 24 0.29 18287 0 573167 21014 \n",
"\n",
" name Description InvoiceDate Country \n",
" role unused_string unused_string unused_string \n",
" 0 MEDIUM CERAMIC TOP STORAGE JAR 2011-01-18 10:01 United Kingdom\n",
" 1 BLACK CANDELABRA T-LIGHT HOLDER 2010-12-07 14:57 Iceland \n",
" 2 AIRLINE BAG VINTAGE JET SET BROW... 2010-12-07 14:57 Iceland \n",
" 3 COLOUR GLASS. STAR T-LIGHT HOLDE... 2010-12-07 14:57 Iceland \n",
" 4 MINI PAINT SET VINTAGE 2010-12-07 14:57 Iceland \n",
" ... ... ... \n",
"397920 LIPSTICK PEN RED 2011-10-12 10:23 United Kingdom\n",
"397921 HAND WARMER SCOTTY DOG DESIGN 2011-10-12 10:23 United Kingdom\n",
"397922 SET OF 3 WOODEN SLEIGH DECORATIO... 2011-10-28 09:29 United Kingdom\n",
"397923 PAINTED METAL STAR WITH HOLLY BE... 2011-10-28 09:29 United Kingdom\n",
"397924 SWISS CHALET TREE DECORATION 2011-10-28 09:29 United Kingdom\n",
"\n",
"\n",
"397925 rows x 9 columns\n",
"memory usage: 57.28 MB\n",
"type: getml.DataFrame"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"full_data = getml.data.DataFrame.from_pandas(full_data_pandas, \"full_data\")\n",
"\n",
"full_data"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### 1.3 Prepare data for getML"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"getML requires that we define *roles* for each of the columns."
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
"full_data.set_role(\"InvoiceDate\", getml.data.roles.time_stamp, time_formats=['%Y-%m-%d %H:%M'])\n",
"full_data.set_role([\"Customer ID\", \"Invoice\"], getml.data.roles.join_key)\n",
"full_data.set_role([\"cancelled\"], getml.data.roles.target)\n",
"full_data.set_role([\"Quantity\", \"Price\"], getml.data.roles.numerical)\n",
"full_data.set_role(\"Country\", getml.data.roles.categorical)\n",
"full_data.set_role(\"Description\", getml.data.roles.text)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The *StockCode* is a 5-digit code that uniquely defines a product. It is hierarchical, meaning that every digit has a meaning. We want to make use of that, so we assign a unit to the stock code, which we can reference in our preprocessors."
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
"full_data.set_unit(\"StockCode\", \"code\")"
]
},
{
"cell_type": "code",
"execution_count": 17,
"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",
"
0
\n",
" \n",
" \n",
"
train
\n",
" \n",
" \n",
"
\n",
" \n",
"
\n",
"
1
\n",
" \n",
" \n",
"
validation
\n",
" \n",
" \n",
"
\n",
" \n",
"
\n",
"
2
\n",
" \n",
" \n",
"
train
\n",
" \n",
" \n",
"
\n",
" \n",
"
\n",
"
3
\n",
" \n",
" \n",
"
validation
\n",
" \n",
" \n",
"
\n",
" \n",
"
\n",
"
4
\n",
" \n",
" \n",
"
validation
\n",
" \n",
" \n",
"
\n",
" \n",
"
\n",
"
\n",
" \n",
" \n",
"
...
\n",
" \n",
" \n",
"
\n",
" \n",
" \n",
"
\n",
"\n",
"
\n",
" infinite number of rows \n",
" \n",
" type: StringColumnView \n",
" \n",
"
\n"
],
"text/plain": [
" \n",
" 0 train \n",
" 1 validation\n",
" 2 train \n",
" 3 validation\n",
" 4 validation\n",
" ... \n",
"\n",
"\n",
"infinite number of rows\n",
"type: StringColumnView"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"split = getml.data.split.random(train=0.7, validation=0.15, test=0.15)\n",
"split"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 2. Predictive modeling\n",
"\n",
"We loaded the data and defined the roles and units. Next, we create a getML pipeline for relational learning."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### 2.1 Define relational model\n",
"\n",
"To get started with relational learning, we need to specify the data model.\n",
"\n",
"In our case, there are two joins we are interested in: \n",
"\n",
"1) We want to take a look at all of the other orders on the same invoice.\n",
"\n",
"2) We want to check out how often a certain customer has cancelled orders in the past. Here, we limit ourselves to the last 90 days. To avoid data leaks, we set a horizon of one day."
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"data model\n",
"