{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "d2b361bf",
   "metadata": {},
   "source": [
    "Copyright (c) 2023 Graphcore Ltd. All rights reserved."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "af14ce5a",
   "metadata": {},
   "source": [
    "# Preprocessing a Tabular Dataset into a PyTorch Geometric Data Object suitable for Fraud Detection\n",
    "\n",
    "This notebook demonstrates how to preprocess a tabular fraud dataset, [IEEE-CIS dataset](https://www.kaggle.com/competitions/ieee-fraud-detection/data), into a PyTorch Geometric  (PyG) data object ready for use in the [Fraud Detection on IPU using RGCN GNN - Training](2_training.ipynb) notebook. The approach is inspired by the [AWS Fraud Detection with GNNs](https://github.com/awslabs/realtime-fraud-detection-with-gnn-on-dgl) project, framing the problem as a node classification task using a heterogeneous graph, where the transaction node types have a label indicating whether they are fraudulent or not.\n",
    "\n",
    "In this notebook, you will learn how to:\n",
    "\n",
    "- Turn tabular transaction data into a PyTorch Geometric heterogeneous dataset object suitable for use in the \"Fraud Detection on IPU using RGCN GNN - Training\" `2_training.ipynb` notebook.\n",
    "\n",
    "This notebook assumes some familiarity with PopTorch as well as PyTorch Geometric. For additional resources please consult:\n",
    "* [PopTorch documentation](https://docs.graphcore.ai/projects/poptorch-user-guide/en/latest/index.html)\n",
    "* [PopTorch examples and tutorials](https://docs.graphcore.ai/en/latest/examples.html#pytorch)\n",
    "* [PyTorch Geometric documentation](https://pytorch-geometric.readthedocs.io/en/latest/)\n",
    "* [PopTorch Geometric documentation](https://docs.graphcore.ai/projects/poptorch-geometric-user-guide/en/latest/index.html)\n",
    "\n",
    "[![Join our Slack Community](https://img.shields.io/badge/Slack-Join%20Graphcore's%20Community-blue?style=flat-square&logo=slack)](https://www.graphcore.ai/join-community)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "db890f52",
   "metadata": {},
   "source": [
    "## Environment setup\n",
    "\n",
    "The best way to run this demo is on Paperspace Gradient’s cloud IPUs because everything is already set up for you. To improve your experience, we preload datasets and pre-install packages. This can take a few minutes. If you experience errors immediately after starting a session, please try restarting the kernel before contacting support. If a problem persists or you want to give us feedback on the content of this notebook, please reach out to through our community of developers using our [Slack channel](https://www.graphcore.ai/join-community) or raise a [GitHub issue](https://github.com/graphcore/Gradient-PyTorch-Geometric/issues).\n",
    "\n",
    "\n",
    "To run the demo using other IPU hardware, you need to have the Poplar SDK enabled. Refer to the [Getting Started guide](https://docs.graphcore.ai/en/latest/getting-started.html#getting-started) for your system for details on how to enable the Poplar SDK. Also refer to the [Jupyter Quick Start guide](https://docs.graphcore.ai/projects/jupyter-notebook-quick-start/en/latest/index.html) for how to set up Jupyter to be able to run this notebook on a remote IPU machine."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "5cfe28b6",
   "metadata": {},
   "source": [
    "In order to improve usability and support for future users, Graphcore would like to collect information about the\n",
    "applications and code being run in this notebook. The following information will be anonymised before being sent to Graphcore:\n",
    "\n",
    "- User progression through the notebook\n",
    "- Notebook details: number of cells, code being run and the output of the cells\n",
    "- Environment details\n",
    "\n",
    "You can disable logging at any time by running `%unload_ext graphcore_cloud_tools.notebook_logging.gc_logger` from any cell.\n",
    "\n"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "5cdcf4b4",
   "metadata": {},
   "source": [
    "## Dependencies and configuration"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "0a7217c2",
   "metadata": {},
   "source": [
    "Install the dependencies the notebook needs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5c72d24a",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install  -r requirements.txt\n",
    "%load_ext graphcore_cloud_tools.notebook_logging.gc_logger"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "b33a842f",
   "metadata": {},
   "source": [
    "To improve your experience, we read in some configuration related to the environment you are running the notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "965ca0a6",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "number_of_ipus = int(os.getenv(\"NUM_AVAILABLE_IPU\", 16))\n",
    "pod_type = os.getenv(\"GRAPHCORE_POD_TYPE\", \"pod16\")\n",
    "executable_cache_dir = os.getenv(\"POPLAR_EXECUTABLE_CACHE_DIR\", \"/tmp/exe_cache/\")\n",
    "dataset_directory = os.getenv(\"DATASETS_DIR\", \".\")\n",
    "checkpoint_directory = os.getenv(\"CHECKPOINT_DIR\", \".\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "62e94d37",
   "metadata": {},
   "source": [
    "Now let's get started."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "0a32779d",
   "metadata": {},
   "source": [
    "## Loading tabular data into PyTorch Geometric\n",
    "\n",
    "Many real world problems start with a tabular dataset. In this section, we will load a tabular dataset, preprocess it into a graph and put it into a PyTorch Geometric data object ready to be used to train a PyTorch Geometric model."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "32152d7e",
   "metadata": {},
   "source": [
    "### Getting the dataset\n",
    "\n",
    "First we need a tabular dataset for fraud detection. We will use the dataset from the [IEEE-CIS Fraud Detection](https://www.kaggle.com/c/ieee-fraud-detection/data) competition on Kaggle.\n",
    "\n",
    "You will need to download the dataset from the [Kaggle competition website](https://www.kaggle.com/c/ieee-fraud-detection/data) and place it in a directory called `raw`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8a97bde6",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os.path as osp\n",
    "import pandas as pd\n",
    "\n",
    "raw_dataset_path = osp.join(dataset_directory, \"ieee-fraud-detection/raw\")\n",
    "\n",
    "dataset_raw_files = [\n",
    "    \"train_transaction.csv\",\n",
    "    \"train_identity.csv\",\n",
    "    \"test_transaction.csv\",\n",
    "    \"test_identity.csv\",\n",
    "]\n",
    "\n",
    "dataset_raw_paths = []\n",
    "for file in dataset_raw_files:\n",
    "    full_path = osp.join(raw_dataset_path, file)\n",
    "    if not os.path.isfile(full_path):\n",
    "        raise FileNotFoundError(\n",
    "            f\"Dataset at path {full_path} not found. Ensure the dataset\"\n",
    "            f\" has been downloaded and unpacked into {raw_dataset_path}\"\n",
    "        )\n",
    "    dataset_raw_paths.append(full_path)\n",
    "\n",
    "train_transaction_df = pd.read_csv(dataset_raw_paths[0])\n",
    "train_identity_df = pd.read_csv(dataset_raw_paths[1])\n",
    "test_transaction_df = pd.read_csv(dataset_raw_paths[2])\n",
    "test_identity_df = pd.read_csv(dataset_raw_paths[3])"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "bc092ce3",
   "metadata": {},
   "source": [
    "We will concatenate the training and test datasets in order to make the PyTorch Geometric graph. Later we will redefine new dataset splits."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "28b1d5ec",
   "metadata": {},
   "outputs": [],
   "source": [
    "transaction_df = pd.concat([train_transaction_df, test_transaction_df], axis=0)\n",
    "identity_df = pd.concat([train_identity_df, test_identity_df], axis=0)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "64f7f7ad",
   "metadata": {},
   "source": [
    "So, we have two tables to work with:\n",
    " * `transaction_df` - properties about the transactions themselves, for example information about card used, or the billing address.\n",
    " * `identity_df` - identity information associated with the transactions, for example digital signature, or network connection information.\n",
    "\n",
    "For more details on this data see the [Kaggle competition forum](https://www.kaggle.com/c/ieee-fraud-detection/discussion/101203) discussing this topic.\n",
    "\n",
    "Let's take a look at the tables themselves:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4f3aaddd",
   "metadata": {},
   "outputs": [],
   "source": [
    "transaction_df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9476115e",
   "metadata": {},
   "outputs": [],
   "source": [
    "identity_df.head()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "38e54cdb",
   "metadata": {},
   "source": [
    "You may notice both tables have some `NaN` values. If the information wasn't available for that particular transaction, the value will be `NaN`.\n",
    "\n",
    "As both tables have transaction IDs in common, we merge both tables into one."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "921517f5",
   "metadata": {},
   "outputs": [],
   "source": [
    "transaction_df = pd.merge(transaction_df, identity_df, on=\"TransactionID\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "dc6f8d58",
   "metadata": {},
   "source": [
    "We then sort the transactions based on their datetime information. When we create the dataset splits we will use the datetime of the transactions to decide how to split the data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b189d0f0",
   "metadata": {},
   "outputs": [],
   "source": [
    "transaction_df.sort_values(\"TransactionDT\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "10669a9b",
   "metadata": {},
   "source": [
    "In the interests of time, for this notebook we will only take the first 10000 samples. See `dataset.py` for the full dataset preprocessing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ac19d9e0",
   "metadata": {},
   "outputs": [],
   "source": [
    "transaction_df = transaction_df.head(10000)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "1b5fb6c6",
   "metadata": {},
   "source": [
    "### Preprocessing the dataset\n",
    "\n",
    "We will frame this fraud detection task as a node classification problem. Each transaction in the table can be a distinct node, with a set of features and a label determining whether it is a fraudulent transaction or not. A transaction node will have some category features, like device type or device info, concatenated with some numerical features, like the transaction amount.\n",
    "\n",
    "As well as transaction nodes, we can construct other node types based on some of the category columns in the table, for example `ProductCD` which represents the produce code or `card1` which represents some card information. Each transaction node will be connected to one node of each of the other node types, constructing a heterogeneous graph. For example, a transaction node will be connected to a single `ProductCD` node, a single `card1` node and to one node of each of the other node types. The columns we don't use to create new node types will be considered as category and numerical features of the transaction nodes themselves.\n",
    "\n",
    "Now, let's preprocess the table, following the above method.\n",
    "\n",
    "First, we filter the transactions which don't have `isFraud` values:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f328e7ba",
   "metadata": {},
   "outputs": [],
   "source": [
    "transaction_df = transaction_df[transaction_df[\"isFraud\"].notna()]\n",
    "transaction_df"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "7087ef64",
   "metadata": {},
   "source": [
    "#### Create the non-target node types\n",
    "\n",
    "We want to create a heterogeneous graph where the node type we are training on are the transaction nodes. The other nodes, or non-target nodes, will be various category columns of the dataset. Specifically, the following columns will be the other node types:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a4b19aa2",
   "metadata": {},
   "outputs": [],
   "source": [
    "non_target_node_types = [\n",
    "    \"card1\",\n",
    "    \"card2\",\n",
    "    \"card3\",\n",
    "    \"card4\",\n",
    "    \"card5\",\n",
    "    \"card6\",\n",
    "    \"ProductCD\",\n",
    "    \"addr1\",\n",
    "    \"addr2\",\n",
    "    \"P_emaildomain\",\n",
    "    \"R_emaildomain\",\n",
    "]"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "3c96e8d2",
   "metadata": {},
   "source": [
    "For each of these columns, we create a new node of that type and connect an edge from the transaction to that node type. If a node of that type with that category already exists, we just connect the edge from the transaction to the existing node."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6dc0ded5",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "\n",
    "get_cat_map = lambda vals: {val: idx for idx, val in enumerate(vals)}\n",
    "\n",
    "\n",
    "def get_edge_list(df, identifier):\n",
    "    # Find number of unique categories for this node type\n",
    "    unique_entries = df[identifier].drop_duplicates().dropna()\n",
    "    # Create a map of category to value\n",
    "    entry_map = get_cat_map(unique_entries)\n",
    "    # Create edge list mapping transaction to node type\n",
    "    edge_list = [[], []]\n",
    "\n",
    "    for idx, transaction in transaction_df.iterrows():\n",
    "        node_type_val = transaction[identifier]\n",
    "        # Don't create nodes for NaN values\n",
    "        if pd.isna(node_type_val):\n",
    "            continue\n",
    "        edge_list[0].append(idx)\n",
    "        edge_list[1].append(entry_map[node_type_val])\n",
    "    return torch.tensor(edge_list, dtype=torch.long)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f3152872",
   "metadata": {},
   "outputs": [],
   "source": [
    "edge_dict = {\n",
    "    node_type: get_edge_list(transaction_df, node_type)\n",
    "    for node_type in non_target_node_types\n",
    "}\n",
    "edge_dict"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "1de8009c",
   "metadata": {},
   "source": [
    "This defines the edge index for each edge type from the transaction nodes.\n",
    "\n",
    "Next we will create the features for the transaction nodes. The columns that we aren't using to create new node types will be transaction features. These columns either have category values or numeric values. We process the category features as concatenated one-hot tensors. All numeric features will be concatenated and then concatenated to the category features.\n",
    "\n",
    "First we define the category columns:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "33081ac3",
   "metadata": {},
   "outputs": [],
   "source": [
    "target_cat_feat_cols = [\n",
    "    \"M1\",\n",
    "    \"M2\",\n",
    "    \"M3\",\n",
    "    \"M4\",\n",
    "    \"M5\",\n",
    "    \"M6\",\n",
    "    \"M7\",\n",
    "    \"M8\",\n",
    "    \"M9\",\n",
    "    \"DeviceType\",\n",
    "    \"DeviceInfo\",\n",
    "    \"id_12\",\n",
    "    \"id_13\",\n",
    "    \"id_14\",\n",
    "    \"id_15\",\n",
    "    \"id_16\",\n",
    "    \"id_17\",\n",
    "    \"id_18\",\n",
    "    \"id_19\",\n",
    "    \"id_20\",\n",
    "    \"id_21\",\n",
    "    \"id_22\",\n",
    "    \"id_23\",\n",
    "    \"id_24\",\n",
    "    \"id_25\",\n",
    "    \"id_26\",\n",
    "    \"id_27\",\n",
    "    \"id_28\",\n",
    "    \"id_29\",\n",
    "    \"id_30\",\n",
    "    \"id_31\",\n",
    "    \"id_32\",\n",
    "    \"id_33\",\n",
    "    \"id_34\",\n",
    "    \"id_35\",\n",
    "    \"id_36\",\n",
    "    \"id_37\",\n",
    "    \"id_38\",\n",
    "]"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "198bc57d",
   "metadata": {},
   "source": [
    "We take the remaining columns as numeric features:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4867cff2",
   "metadata": {},
   "outputs": [],
   "source": [
    "excl_cols = [\"TransactionID\", \"isFraud\", \"TransactionDT\"]\n",
    "\n",
    "target_numeric_feat_cols = [\n",
    "    column\n",
    "    for column in transaction_df.columns\n",
    "    if column not in non_target_node_types + excl_cols + target_cat_feat_cols\n",
    "]\n",
    "print(\" \".join(target_numeric_feat_cols))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "097d5eda",
   "metadata": {},
   "source": [
    "Create a dataframe of just these columns:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f5c847ec",
   "metadata": {},
   "outputs": [],
   "source": [
    "transaction_feat_df = transaction_df[\n",
    "    target_numeric_feat_cols + target_cat_feat_cols\n",
    "].copy()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "5cbc5836",
   "metadata": {},
   "source": [
    "Make any `NaN` values `0`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3d0e770b",
   "metadata": {},
   "outputs": [],
   "source": [
    "transaction_feat_df = transaction_feat_df.fillna(0)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "9156e724",
   "metadata": {},
   "source": [
    "As mentioned, we will process the category columns into one-hot tensors and concatenate them."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0d57fe12",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "\n",
    "\n",
    "def get_cat_feat(df, key):\n",
    "    categories = set(row[key] for _, row in df.iterrows())\n",
    "    mapping = {cat: i for i, cat in enumerate(categories)}\n",
    "\n",
    "    x = torch.zeros((len(df), len(mapping)), dtype=torch.float32)\n",
    "    for i, row in df.iterrows():\n",
    "        x[i, mapping[row[key]]] = 1\n",
    "    return x\n",
    "\n",
    "\n",
    "cat_features = [get_cat_feat(transaction_feat_df, key) for key in target_cat_feat_cols]\n",
    "cat_feats = torch.cat(cat_features, dim=-1)\n",
    "cat_feats[0]"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "7ba764b0",
   "metadata": {},
   "source": [
    "Process the numeric features:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a9efc780",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "\n",
    "def process_val(col, val):\n",
    "    if pd.isna(val):\n",
    "        return 0.0\n",
    "\n",
    "    if col == \"TransactionAmt\":\n",
    "        val = np.log10(val)\n",
    "    return val\n",
    "\n",
    "\n",
    "num_feats = [\n",
    "    list(\n",
    "        map(\n",
    "            process_val,\n",
    "            target_numeric_feat_cols,\n",
    "            [row[feat] for feat in target_numeric_feat_cols],\n",
    "        )\n",
    "    )\n",
    "    for _, row in transaction_feat_df.iterrows()\n",
    "]\n",
    "num_feats = torch.tensor(num_feats, dtype=torch.float32)\n",
    "num_feats.shape"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "f98f7a78",
   "metadata": {},
   "source": [
    "Finally, concatenate the category and numeric features together:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e3d8acd8",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch.nn.functional as F\n",
    "\n",
    "transaction_feats = torch.cat((cat_feats, num_feats), -1)\n",
    "transaction_feats.shape"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "79a0b35a",
   "metadata": {},
   "source": [
    "We now have all the pieces to create the dataset, the transaction features and the edge indices for each transaction to node type edge."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "e0b7e7a9",
   "metadata": {},
   "source": [
    "### Creating a PyTorch Geometric dataset\n",
    "\n",
    "Now we can put the transaction features and the edge indices into a PyTorch Geometric `HeteroData` object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "26959614",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch_geometric.data import HeteroData\n",
    "\n",
    "data = HeteroData()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "00234de9",
   "metadata": {},
   "source": [
    "Set the features and labels for the transaction nodes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "689db7c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "data[\"transaction\"].num_nodes = len(transaction_df)\n",
    "data[\"transaction\"].x = transaction_feats\n",
    "data[\"transaction\"].y = torch.tensor(transaction_df[\"isFraud\"].astype(\"long\"))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "8f046bae",
   "metadata": {},
   "source": [
    "Then, for each of the other node types we, create the nodes and the edges:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9dac2749",
   "metadata": {},
   "outputs": [],
   "source": [
    "for node_type in non_target_node_types:\n",
    "    data[\"transaction\", \"to\", node_type].edge_index = edge_dict[node_type]\n",
    "    data[node_type].num_nodes = edge_dict[node_type][1].max() + 1\n",
    "    # Create dummy features for the non-transaction node types\n",
    "    data[node_type].x = torch.zeros((edge_dict[node_type][1].max() + 1, 1))"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "becf560e",
   "metadata": {},
   "source": [
    "We can validate the data we have created:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "321a0fca",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert data.validate()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "7d18b0bb",
   "metadata": {},
   "source": [
    "Now let's see what the resulting graph looks like:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cdeb9b1a",
   "metadata": {},
   "outputs": [],
   "source": [
    "data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "79641f6f",
   "metadata": {},
   "outputs": [],
   "source": [
    "data.num_nodes"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "3218462e",
   "metadata": {},
   "source": [
    "The graph looks as expected. There are a number of node types, but only the transaction nodes have labels. Each transaction is connected to a node of a different node type. "
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "98db4ad9",
   "metadata": {},
   "source": [
    "## Visualizing the graph"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "67090747",
   "metadata": {},
   "source": [
    "We can visualise the heterogeneous graph we have created from the tabular data.\n",
    "\n",
    "Let's just select a fraction of the graph for visualizing:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20b85cc6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch_geometric.transforms import RemoveIsolatedNodes\n",
    "\n",
    "data = data.subgraph({\"transaction\": torch.arange(0, 3)})\n",
    "data = RemoveIsolatedNodes()(data)\n",
    "data"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "721f97b1",
   "metadata": {},
   "source": [
    "We can use NetworkX to visualise this graph:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "35096262",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "import networkx as nx\n",
    "from matplotlib import pyplot as plt\n",
    "from torch_geometric.utils import to_networkx\n",
    "\n",
    "# Convert to homogeneous\n",
    "data_homogeneous = data.to_homogeneous()\n",
    "g = to_networkx(data_homogeneous)\n",
    "# Use node types as colour map\n",
    "colour_map = data_homogeneous.node_type\n",
    "\n",
    "pos = nx.spring_layout(g)\n",
    "\n",
    "# Split the nodes by node type and add some randomness to separate the nodes\n",
    "for i in range(0, len(colour_map)):\n",
    "    if colour_map[i] != 0:\n",
    "        pos[i][0] += np.cos(colour_map[i] / 2) * 10 + random.randint(-1, 1)\n",
    "        pos[i][1] += np.sin(colour_map[i] / 2) * 10 + random.randint(-1, 1)\n",
    "    else:\n",
    "        pos[i][0] += random.randint(-3, 3)\n",
    "        pos[i][1] += random.randint(-3, 3)\n",
    "\n",
    "nx.draw_networkx(g, pos=pos, node_color=colour_map * 40, cmap=plt.cm.tab20)\n",
    "plt.show()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "5143f1b7",
   "metadata": {},
   "source": [
    "Nodes 0 - 2 represent the transaction nodes. As expected, each transaction node is connected out to the nodes of the other types, each represented with a different colour."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "7ece4041",
   "metadata": {},
   "source": [
    "## Conclusion\n",
    "\n",
    "In this notebook we have preprocessed a tabular dataset into a PyTorch Geometric `HeteroData` object, ready for training. Specifically we have:\n",
    "\n",
    " - Loaded the [IEEE-CIS Fraud Detection](https://www.kaggle.com/c/ieee-fraud-detection/data) dataset,\n",
    " - Created edge indices for each edge type from particular columns,\n",
    " - Created transaction features based on category and numeric columns,\n",
    " - Created a PyTorch Geometric `HeteroData` object containing these features and edges,\n",
    " - Visualised the resulting graph.\n",
    "\n",
    "To preprocess and cache the entire dataset use the `dataset.py` script.\n",
    "\n",
    "This dataset is used for training a GNN as shown in the \"Fraud Detection on IPU using RGCN GNN - Training\" `2_training.ipynb` notebook."
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
