{ "cells": [ { "cell_type": "markdown", "id": "israeli-spyware", "metadata": {}, "source": [ "## Build your own training/testing split\n", "\n", "#### Date: 2021.10.07\n", "\n", "When working with machine learning data, splitting into a \"train\", \"dev\" (or validation) and \"test\") set is important. Models use **train** data to learn representations and update their parameters; **dev** or validation data is reserved to see how the model may perform on unknown predictions. While it may not be explicitly trained on, it can be used as a stopping criteria, for hyper-parameter tuning, or as a simple sanity check. Lastly, **test** data is always reserved, hidden from the model, as a final pass to see what models perform best.\n", "\n", "Lightwood supports a variety of **encoders** (Feature engineering procedures) and **mixers** (predictor algorithms that go from feature vectors to the target). Given the diversity of algorithms, it is appropriate to split data into these three categories when *preparing* encoders or *fitting* mixers.\n", "\n", "Our default approach stratifies labeled data to ensure your train, validation, and test sets are equally represented in all classes. However, in many instances you may want a custom technique to build your own splits. We've included the `splitter` functionality (default found in `lightwood.data.splitter`) to enable you to build your own.\n", "\n", "In the following problem, we shall work with a Kaggle dataset around credit card fraud (found [here](https://www.kaggle.com/mlg-ulb/creditcardfraud)). Fraud detection is difficult because the events we are interested in catching are thankfully rare events. Because of that, there is a large **imbalance of classes** (in fact, in this dataset, less than 1% of the data are the rare-event).\n", "\n", "In a supervised technique, we may want to ensure our training data sees the rare event of interest. A random shuffle could potentially miss rare events. We will implement **SMOTE** to increase the number of positive classes in our training data.\n", "\n", "Let's get started!" ] }, { "cell_type": "code", "execution_count": 1, "id": "interim-discussion", "metadata": { "execution": { "iopub.execute_input": "2024-05-07T17:12:19.505399Z", "iopub.status.busy": "2024-05-07T17:12:19.505191Z", "iopub.status.idle": "2024-05-07T17:12:23.726284Z", "shell.execute_reply": "2024-05-07T17:12:23.725617Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:lightwood-2306:No torchvision detected, image helpers not supported.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:lightwood-2306:No torchvision/pillow detected, image encoder not supported\u001b[0m\n" ] } ], "source": [ "import numpy as np\n", "import pandas as pd\n", "import torch\n", "import nltk\n", "import matplotlib.pyplot as plt\n", "\n", "import os\n", "import sys\n", "\n", "# Lightwood modules\n", "import lightwood as lw\n", "from lightwood import ProblemDefinition, \\\n", " JsonAI, \\\n", " json_ai_from_problem, \\\n", " code_from_json_ai, \\\n", " predictor_from_code\n", "\n", "import imblearn # Vers 0.5.0 minimum requirement" ] }, { "cell_type": "markdown", "id": "decimal-techno", "metadata": {}, "source": [ "### 1) Load your data\n", "\n", "Lightwood works with `pandas` DataFrames. We can use pandas to load our data. Please download the dataset from the above link and place it in a folder called `data/` where this notebook is located." ] }, { "cell_type": "code", "execution_count": 2, "id": "foreign-orchestra", "metadata": { "execution": { "iopub.execute_input": "2024-05-07T17:12:23.729268Z", "iopub.status.busy": "2024-05-07T17:12:23.728955Z", "iopub.status.idle": "2024-05-07T17:12:30.225282Z", "shell.execute_reply": "2024-05-07T17:12:30.224479Z" } }, "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", "
TimeV1V2V3V4V5V6V7V8V9...V21V22V23V24V25V26V27V28AmountClass
00.0-1.359807-0.0727812.5363471.378155-0.3383210.4623880.2395990.0986980.363787...-0.0183070.277838-0.1104740.0669280.128539-0.1891150.133558-0.021053149.620
10.01.1918570.2661510.1664800.4481540.060018-0.082361-0.0788030.085102-0.255425...-0.225775-0.6386720.101288-0.3398460.1671700.125895-0.0089830.0147242.690
21.0-1.358354-1.3401631.7732090.379780-0.5031981.8004990.7914610.247676-1.514654...0.2479980.7716790.909412-0.689281-0.327642-0.139097-0.055353-0.059752378.660
31.0-0.966272-0.1852261.792993-0.863291-0.0103091.2472030.2376090.377436-1.387024...-0.1083000.005274-0.190321-1.1755750.647376-0.2219290.0627230.061458123.500
42.0-1.1582330.8777371.5487180.403034-0.4071930.0959210.592941-0.2705330.817739...-0.0094310.798278-0.1374580.141267-0.2060100.5022920.2194220.21515369.990
\n", "

5 rows × 31 columns

\n", "
" ], "text/plain": [ " Time V1 V2 V3 V4 V5 V6 V7 \\\n", "0 0.0 -1.359807 -0.072781 2.536347 1.378155 -0.338321 0.462388 0.239599 \n", "1 0.0 1.191857 0.266151 0.166480 0.448154 0.060018 -0.082361 -0.078803 \n", "2 1.0 -1.358354 -1.340163 1.773209 0.379780 -0.503198 1.800499 0.791461 \n", "3 1.0 -0.966272 -0.185226 1.792993 -0.863291 -0.010309 1.247203 0.237609 \n", "4 2.0 -1.158233 0.877737 1.548718 0.403034 -0.407193 0.095921 0.592941 \n", "\n", " V8 V9 ... V21 V22 V23 V24 V25 \\\n", "0 0.098698 0.363787 ... -0.018307 0.277838 -0.110474 0.066928 0.128539 \n", "1 0.085102 -0.255425 ... -0.225775 -0.638672 0.101288 -0.339846 0.167170 \n", "2 0.247676 -1.514654 ... 0.247998 0.771679 0.909412 -0.689281 -0.327642 \n", "3 0.377436 -1.387024 ... -0.108300 0.005274 -0.190321 -1.175575 0.647376 \n", "4 -0.270533 0.817739 ... -0.009431 0.798278 -0.137458 0.141267 -0.206010 \n", "\n", " V26 V27 V28 Amount Class \n", "0 -0.189115 0.133558 -0.021053 149.62 0 \n", "1 0.125895 -0.008983 0.014724 2.69 0 \n", "2 -0.139097 -0.055353 -0.059752 378.66 0 \n", "3 -0.221929 0.062723 0.061458 123.50 0 \n", "4 0.502292 0.219422 0.215153 69.99 0 \n", "\n", "[5 rows x 31 columns]" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Load the data\n", "data = pd.read_csv(\"https://mindsdb-example-data.s3.eu-west-2.amazonaws.com/jupyter/creditcard.csv.zip\")\n", "data.head()" ] }, { "cell_type": "markdown", "id": "rental-contribution", "metadata": {}, "source": [ "We see **31 columns**, most of these columns appear numerical. Due to confidentiality reasons, the Kaggle dataset mentions that the columns labeled $V_i$ indicate principle components (PCs) from a PCA analysis of the original data from the credit card company. There is also a \"Time\" and \"Amount\", two original features that remained. The time references time after the first transaction in the dataset, and amount is how much money was considered in the transaction. \n", "\n", "You can also see a heavy imbalance in the two classes below:" ] }, { "cell_type": "code", "execution_count": 3, "id": "cathedral-mills", "metadata": { "execution": { "iopub.execute_input": "2024-05-07T17:12:30.228432Z", "iopub.status.busy": "2024-05-07T17:12:30.227800Z", "iopub.status.idle": "2024-05-07T17:12:30.592309Z", "shell.execute_reply": "2024-05-07T17:12:30.591642Z" } }, "outputs": [ { "data": { "text/plain": [ "Text(0.5, 1.0, 'Distribution of Classes')" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjoAAAHHCAYAAAC2rPKaAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAArr0lEQVR4nO3de1TVdb7/8dcGuYhchPRAKIHXFDNsKaKRmTOYo6aVdsacxojSzplIR9GcNBOzLGfKNHM3ljXi2JmT5ozWyfIS6pCNc0S8LsnyLmpi5gEEHVH4/v5ouX8RXtiwt3v72c/HWqw1+/v97u9+4yJ4zveyt82yLEsAAAAG8vP0AAAAAO5C6AAAAGMROgAAwFiEDgAAMBahAwAAjEXoAAAAYxE6AADAWIQOAAAwFqEDAACMRegAN4Bp06bJZrNdl9e65557dM899zgeb9iwQTabTcuWLbsur//YY48pISHhurxWfZWXl2vkyJGKiYmRzWbT2LFjXbLfnJwc2Ww2HTp0yCX7A0DoANfdpT9ml76Cg4MVGxurfv36ae7cuTpz5oxLXuf48eOaNm2atm/f7pL9uZI3z1YXL7/8snJycvSb3/xGixcv1ogRI666fVVVlRYuXKh77rlHUVFRCgoKUkJCgjIyMrRly5brNDXgmxp5egDAV02fPl2tWrXShQsXdOLECW3YsEFjx47V66+/ro8//li33367Y9spU6bo2WefdWr/x48f1wsvvKCEhAR16dKlzs9bs2aNU69TH1ebbcGCBaqurnb7DA2xbt069ejRQ9nZ2dfc9ty5cxoyZIhWrVqlu+++W5MnT1ZUVJQOHTqkpUuXatGiRTpy5Ihatmx5HSYHfA+hA3hI//791a1bN8fjSZMmad26dbrvvvs0ePBgffXVV2rcuLEkqVGjRmrUyL3/uZ49e1YhISEKDAx06+tcS0BAgEdfvy5OnjypxMTEOm37zDPPaNWqVZo9e3atU1zZ2dmaPXu2GyYEcAmnrgAv8rOf/UzPP/+8Dh8+rPfff9+x/HLX6Kxdu1Z33XWXmjZtqtDQUN16662aPHmypB+uq0lOTpYkZWRkOE6T5eTkSPrhOpzbbrtNBQUFuvvuuxUSEuJ47k+v0bmkqqpKkydPVkxMjJo0aaLBgwerqKioxjYJCQl67LHHaj33x/u81myXu0anoqJC48ePV1xcnIKCgnTrrbfqtddek2VZNbaz2Wx6+umntWLFCt12220KCgpSp06dtGrVqsv/g//EyZMn9cQTTyg6OlrBwcFKSkrSokWLHOsvXa908OBBrVy50jH7la6pOXr0qN5++2317dv3stfx+Pv7a8KECVc9mvPRRx9p4MCBio2NVVBQkNq0aaMXX3xRVVVVNbbbu3evhg4dqpiYGAUHB6tly5Z6+OGHVVpa6tjmaj8zl5w/f17Z2dlq27atgoKCFBcXp4kTJ+r8+fM1tqvLvgBvwBEdwMuMGDFCkydP1po1azRq1KjLbrN7927dd999uv322zV9+nQFBQVp3759+vLLLyVJHTt21PTp0zV16lQ9+eST6tWrlyTpzjvvdOzj+++/V//+/fXwww/r17/+taKjo68614wZM2Sz2fS73/1OJ0+e1Jw5c5SWlqbt27c7jjzVRV1m+zHLsjR48GCtX79eTzzxhLp06aLVq1frmWee0bFjx2odEdm4caP+9re/6amnnlJYWJjmzp2roUOH6siRI7rpppuuONe5c+d0zz33aN++fXr66afVqlUrffjhh3rsscdUUlKi3/72t+rYsaMWL16scePGqWXLlho/frwkqXnz5pfd52effaaLFy9e8xqeq8nJyVFoaKiysrIUGhqqdevWaerUqSorK9Orr74qSaqsrFS/fv10/vx5jR49WjExMTp27Jg++eQTlZSUKCIi4po/M5JUXV2twYMHa+PGjXryySfVsWNH7dq1S7Nnz9Y333yjFStWSLr2zx/gVSwA19XChQstSVZ+fv4Vt4mIiLDuuOMOx+Ps7Gzrx/+5zp4925Jkfffdd1fcR35+viXJWrhwYa11vXv3tiRZ8+fPv+y63r17Ox6vX7/ekmS1aNHCKisrcyxfunSpJcl64403HMvi4+Ot9PT0a+7zarOlp6db8fHxjscrVqywJFkvvfRSje0eeughy2azWfv27XMsk2QFBgbWWLZjxw5LkvXmm2/Weq0fmzNnjiXJev/99x3LKisrrZ49e1qhoaE1vvf4+Hhr4MCBV92fZVnWuHHjLEnWtm3brrmtZf3/n42DBw86lp09e7bWdv/xH/9hhYSEWP/6178sy7Ksbdu2WZKsDz/88Ir7rsvPzOLFiy0/Pz/riy++qLF8/vz5liTryy+/rPO+AG/BqSvAC4WGhl717qumTZtK+uG0Rn0v3A0KClJGRkadt3/00UcVFhbmePzQQw/p5ptv1qefflqv16+rTz/9VP7+/hozZkyN5ePHj5dlWfrss89qLE9LS1ObNm0cj2+//XaFh4frwIED13ydmJgYDR8+3LEsICBAY8aMUXl5uf7+9787PXtZWZkk1fh3c9aPj5adOXNGp06dUq9evXT27Fnt2bNHkhQRESFJWr16tc6ePXvZ/dTlZ+bDDz9Ux44d1aFDB506dcrx9bOf/UyStH79+jrvC/AWhA7ghcrLy6/6x3HYsGFKTU3VyJEjFR0drYcfflhLly516o9OixYtnLrwuF27djUe22w2tW3b1u3v+XL48GHFxsbW+vfo2LGjY/2P3XLLLbX2ERkZqf/7v/+75uu0a9dOfn41fy1e6XXqIjw8XJIa9JYBu3fv1oMPPqiIiAiFh4erefPm+vWvfy1JjutvWrVqpaysLL377rtq1qyZ+vXrJ7vdXuP6nLr8zOzdu1e7d+9W8+bNa3y1b99e0g/XMNV1X4C3IHQAL3P06FGVlpaqbdu2V9ymcePGysvL0+eff64RI0Zo586dGjZsmPr27VvrItWr7cPVrvSmhnWdyRX8/f0vu9z6yYXL10OHDh0kSbt27arX80tKStS7d2/t2LFD06dP1//8z/9o7dq1+v3vfy9JNcJi1qxZ2rlzpyZPnqxz585pzJgx6tSpk44ePSqpbj8z1dXV6ty5s9auXXvZr6eeeqrO+wK8BaEDeJnFixdLkvr163fV7fz8/PTzn/9cr7/+ugoLCzVjxgytW7fOcXrB1e+kvHfv3hqPLcvSvn37atwhFRkZqZKSklrP/enREGdmi4+P1/Hjx2sdFbl02iY+Pr7O+7rW6+zdu7fWUYmGvE7//v3l7+9f4w46Z2zYsEHff/+9cnJy9Nvf/lb33Xef0tLSFBkZedntO3furClTpigvL09ffPGFjh07pvnz5zvWX+tnpk2bNjp9+rR+/vOfKy0trdbXrbfeWud9Ad6C0AG8yLp16/Tiiy+qVatWeuSRR6643enTp2stu/TGe5duA27SpIkkXTY86uPPf/5zjdhYtmyZvv32W/Xv39+xrE2bNvrnP/+pyspKx7JPPvmk1m3ozsw2YMAAVVVVad68eTWWz549WzabrcbrN8SAAQN04sQJLVmyxLHs4sWLevPNNxUaGqrevXs7vc+4uDiNGjVKa9as0ZtvvllrfXV1tWbNmuU46vJTl45O/fhoVGVlpd56660a25WVlenixYs1lnXu3Fl+fn6On4e6/Mz88pe/1LFjx7RgwYJa2547d04VFRV13hfgLbi9HPCQzz77THv27NHFixdVXFysdevWae3atYqPj9fHH3+s4ODgKz53+vTpysvL08CBAxUfH6+TJ0/qrbfeUsuWLXXXXXdJ+iE6mjZtqvnz5yssLExNmjRRSkqKWrVqVa95o6KidNdddykjI0PFxcWaM2eO2rZtW+MW+JEjR2rZsmX6xS9+oV/+8pfav3+/3n///RoXBzs726BBg9SnTx8999xzOnTokJKSkrRmzRp99NFHGjt2bK1919eTTz6pt99+W4899pgKCgqUkJCgZcuW6csvv9ScOXPqfUHxrFmztH//fo0ZM0Z/+9vfdN999ykyMlJHjhzRhx9+qD179ujhhx++7HPvvPNORUZGKj09XWPGjJHNZtPixYtrnYZbt26dnn76af37v/+72rdvr4sXL2rx4sXy9/fX0KFDJdXtZ2bEiBFaunSp/vM//1Pr169XamqqqqqqtGfPHi1dulSrV69Wt27d6rQvwGt49J4vwAdduoX40ldgYKAVExNj9e3b13rjjTdq3MZ8yU9vL8/NzbXuv/9+KzY21goMDLRiY2Ot4cOHW998802N53300UdWYmKi1ahRoxq3c/fu3dvq1KnTZee70u3l//3f/21NmjTJ+rd/+zercePG1sCBA63Dhw/Xev6sWbOsFi1aWEFBQVZqaqq1ZcuWWvu82mw/vb3csizrzJkz1rhx46zY2FgrICDAateunfXqq69a1dXVNbaTZGVmZtaa6Uq3vf9UcXGxlZGRYTVr1swKDAy0OnfufNlb4Ot6e/klFy9etN59912rV69eVkREhBUQEGDFx8dbGRkZNW49v9zt5V9++aXVo0cPq3HjxlZsbKw1ceJEa/Xq1ZYka/369ZZlWdaBAwesxx9/3GrTpo0VHBxsRUVFWX369LE+//xzx37q+jNTWVlp/f73v7c6depkBQUFWZGRkVbXrl2tF154wSotLXVqX4A3sFmWB67QAwAAuA64RgcAABiL0AEAAMYidAAAgLEIHQAAYCxCBwAAGIvQAQAAxvL5Nwysrq7W8ePHFRYW5vK3zAcAAO5hWZbOnDmj2NjYWh/G+2M+HzrHjx9X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UkBAgH7zm99o5syZLh8QAACgvpwOncDAQL3xxht65ZVXtH//fklSmzZtFBIS4vLhAAAAGqLOp66qqqq0c+dOnTt3TpIUEhKizp07q3PnzrLZbNq5c6eqq6vdNigAAICz6hw6ixcv1uOPP67AwMBa6wICAvT444/rL3/5i0uHAwAAaIg6h857772nCRMmyN/fv9a6S7eXv/POOy4dzhlnz55VfHy8JkyY4LEZAACAd6lz6Hz99dfq0aPHFdcnJyc7PhLCE2bMmHHV+QAAgO+pc+hUVFSorKzsiuvPnDmjs2fPumQoZ+3du1d79uxR//79PfL6AADAO9U5dNq1a6d//OMfV1y/ceNGtWvXzukB8vLyNGjQIMXGxspms2nFihW1trHb7UpISFBwcLBSUlK0efPmGusnTJigV155xenXBgAAZqtz6PzqV7/SlClTtHPnzlrrduzYoalTp+pXv/qV0wNUVFQoKSlJdrv9suuXLFmirKwsZWdna+vWrUpKSlK/fv108uRJSdJHH32k9u3bq3379k6/NgAAMJvNsiyrLhteuHBB9957rzZu3Ki0tDR16NBBkrRnzx59/vnnSk1N1dq1axUQEFD/YWw2LV++XA888IBjWUpKipKTkzVv3jxJUnV1teLi4jR69Gg9++yzmjRpkt5//335+/urvLxcFy5c0Pjx4zV16tTLvsb58+d1/vx5x+OysjLFxcWptLRU4eHh9Z79chKeXenS/Zns0MyBnh4BAHADKSsrU0RExDX/ftf5iE5AQIDWrFmjGTNm6Ntvv9U777yjt99+W99++61mzJihNWvWNChyLqeyslIFBQVKS0v7/wP7+SktLU2bNm2SJL3yyisqKirSoUOH9Nprr2nUqFFXjJxL20dERDi+4uLiXDozAADwHk69M3JAQIAmTpyoiRMnumueGk6dOqWqqipFR0fXWB4dHa09e/bUa5+TJk1SVlaW4/GlIzoAAMA8Tn8EhDd77LHHrrlNUFCQgoKC3D8MAADwOKc/vfx6atasmfz9/VVcXFxjeXFxsWJiYjw0FQAAuFF4degEBgaqa9euys3NdSyrrq5Wbm6uevbs6cHJAADAjcDjp67Ky8u1b98+x+ODBw9q+/btioqK0i233KKsrCylp6erW7du6t69u+bMmaOKigplZGQ06HXtdrvsdruqqqoa+i0AAAAvVefb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drZ27tzZ4O8DgGcROgC8Urt27WSz2Zy64HjTpk165JFHNGDAAH3yySfatm2bnnvuOVVWVjq2mTZtmnbv3q2BAwdq3bp1SkxM1PLlyyVJI0eO1IEDBzRixAjt2rVL3bp105tvvuny7w3A9WOz6vt/mwDAzfr3769du3bp66+/rnWdTklJiZo2bSqbzably5frgQce0KxZs/TWW2/VOEozcuRILVu2TCUlJZd9jeHDh6uiokIff/xxrXWTJk3SypUrObID3MA4ogPAa9ntdlVVVal79+7661//qr179+qrr77S3Llz1bNnz1rbt2vXTkeOHNEHH3yg/fv3a+7cuY6jNZJ07tw5Pf3009qwYYMOHz6sL7/8Uvn5+erYsaMkaezYsVq9erUOHjyorVu3av369Y51AG5MXIwMwGu1bt1aW7du1YwZMzR+/Hh9++23at68ubp27ao//vGPtbYfPHiwxo0bp6efflrnz5/XwIED9fzzz2vatGmSJH9/f33//fd69NFHVVxcrGbNmmnIkCF64YUXJElVVVXKzMzU0aNHFR4erl/84heaPXv29fyWAbgYp64AAICxOHUFAACMRegAAABjEToAAMBYhA4AADAWoQMAAIxF6AAAAGMROgAAwFiEDgAAMBahAwAAjEXoAAAAYxE6AADAWIQOAAAw1v8DiqovSrkayC8AAAAASUVORK5CYII=", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "f = plt.figure()\n", "ax = f.add_subplot(1,1,1)\n", "ax.hist(data['Class'], bins = [-0.1, 0.1, 0.9, 1.1], log=True)\n", "ax.set_ylabel(\"Log Counts\")\n", "ax.set_xticks([0, 1])\n", "ax.set_xticklabels([\"0\", \"1\"])\n", "ax.set_xlabel(\"Class\")\n", "ax.set_title(\"Distribution of Classes\")" ] }, { "cell_type": "markdown", "id": "exact-timeline", "metadata": {}, "source": [ "### 2) Create a JSON-AI default object\n", "We will now create JSON-AI syntax for our problem based on its specifications. We can do so by setting up a ``ProblemDefinition``. The ``ProblemDefinition`` allows us to specify the target, the column we intend to predict, along with other details. \n", "\n", "The end goal of JSON-AI is to provide **a set of instructions on how to compile a machine learning pipeline*.\n", "\n", "Our target here is called \"**Class**\", which indicates \"0\" for no fraud and \"1\" for fraud. We'll generate the JSON-AI with the minimal syntax:" ] }, { "cell_type": "code", "execution_count": 4, "id": "medieval-zambia", "metadata": { "execution": { "iopub.execute_input": "2024-05-07T17:12:30.595049Z", "iopub.status.busy": "2024-05-07T17:12:30.594645Z", "iopub.status.idle": "2024-05-07T17:13:39.928759Z", "shell.execute_reply": "2024-05-07T17:13:39.927965Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Analyzing a sample of 18424\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:from a total population of 284807, this is equivalent to 6.5% of your data.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Using 3 processes to deduct types.\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: Time\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V3\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V6\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column Time has data type integer\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V1\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V3 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V4\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V6 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V7\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V4 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V5\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V1 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V2\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V5 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V9\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V7 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V8\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V9 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V10\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V2 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V12\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V8 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V15\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V10 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V11\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V12 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V13\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V11 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V18\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V15 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V16\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V18 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V19\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V13 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V14\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V16 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V17\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V17 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V19 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V20\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V14 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V21\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V24\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V24 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V25\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V20 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V21 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V22\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V27\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V22 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V23\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V25 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V26\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V27 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: V28\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V23 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: Class\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V28 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Infering type for: Amount\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column V26 has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column Class has data type binary\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:type_infer-2306:Column Amount has data type float\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:dataprep_ml-2306:Starting statistical analysis\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:dataprep_ml-2306:Finished statistical analysis\u001b[0m\n" ] } ], "source": [ "# Setup the problem definition\n", "problem_definition = {\n", " 'target': 'Class',\n", "}\n", "\n", "# Generate the j{ai}son syntax\n", "json_ai = json_ai_from_problem(data, problem_definition)\n" ] }, { "cell_type": "markdown", "id": "deadly-rotation", "metadata": {}, "source": [ "Lightwood looks at each of the many columns and indicates they are mostly float, with exception of \"**Class**\" which is binary.\n", "\n", "You can observe the JSON-AI if you run the command `print(json_ai.to_json())`. Given there are many input features, we won't print it out." ] }, { "cell_type": "markdown", "id": "immune-clone", "metadata": {}, "source": [ "These are the only elements required to get off the ground with JSON-AI. However, we're interested in making a *custom* approach. So, let's make this syntax a file, and introduce our own changes." ] }, { "cell_type": "markdown", "id": "massive-divide", "metadata": {}, "source": [ "### 3) Build your own splitter module\n", "\n", "For Lightwood, the goal of a splitter is to intake an initial dataset (pre-processed ideally, although you can run the pre-processor on each DataFrame within the splitter) and return a dictionary with the keys \"train\", \"test\", and \"dev\" (at minimum). Subsequent steps of the pipeline expect the keys \"train\", \"test\", and \"dev\", so it's important you assign datasets to these as necessary. \n", "\n", "We're going to introduce SMOTE sampling in our splitter. SMOTE allows you to quickly learn an approximation to make extra \"samples\" that mimic the undersampled class. \n", "\n", "We will use the package `imblearn` and `scikit-learn` to quickly create a train/test split and apply SMOTE to our training data only.\n", "\n", "**NOTE** This is simply an example of things you can do with the splitter; whether SMOTE sampling is ideal for your problem depends on the question you're trying to answer!" ] }, { "cell_type": "code", "execution_count": 5, "id": "4411ee53", "metadata": { "execution": { "iopub.execute_input": "2024-05-07T17:13:39.932371Z", "iopub.status.busy": "2024-05-07T17:13:39.932103Z", "iopub.status.idle": "2024-05-07T17:13:39.937546Z", "shell.execute_reply": "2024-05-07T17:13:39.936875Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing MyCustomSplitter.py\n" ] } ], "source": [ "%%writefile MyCustomSplitter.py\n", "\n", "from type_infer.dtype import dtype\n", "import pandas as pd\n", "import numpy as np\n", "from typing import List, Dict\n", "from itertools import product\n", "from lightwood.api.types import TimeseriesSettings\n", "from lightwood.helpers.log import log\n", "\n", "\n", "from imblearn.over_sampling import SMOTE\n", "from sklearn.model_selection import train_test_split\n", "\n", "\n", "def MySplitter(\n", " data: pd.DataFrame,\n", " target: str,\n", " pct_train: float = 0.8,\n", " pct_dev: float = 0.1,\n", " seed: int = 1,\n", ") -> Dict[str, pd.DataFrame]:\n", " \"\"\"\n", " Custom splitting function\n", "\n", "\n", " :param data: Input data\n", " :param target: Name of the target\n", " :param pct_train: Percentage of data reserved for training, taken out of full data\n", " :param pct_dev: Percentage of data reserved for dev, taken out of train data\n", " :param seed: Random seed for reproducibility\n", "\n", " :returns: A dictionary containing the keys train, test and dev with their respective data frames.\n", " \"\"\"\n", "\n", " # Shuffle the data\n", " data = data.sample(frac=1, random_state=seed).reset_index(drop=True)\n", "\n", " # Split into feature columns + target\n", " X = data.iloc[:, data.columns != target] # .values\n", " y = data[target] # .values\n", "\n", " # Create a train/test split\n", " X2, X_test, y2, y_test = train_test_split(\n", " X, y, train_size=pct_train, random_state=seed, stratify=data[target]\n", " )\n", "\n", " X_train, X_dev, y_train, y_dev = train_test_split(\n", " X2, y2, test_size=pct_dev, random_state=seed, stratify=y2\n", " )\n", "\n", " # Create a SMOTE model and bump up underbalanced class JUST for train data\n", " SMOTE_model = SMOTE(random_state=seed)\n", "\n", " Xtrain_mod, ytrain_mod = SMOTE_model.fit_resample(X_train, y_train.ravel())\n", "\n", " Xtrain_mod[target] = ytrain_mod\n", " X_test[target] = y_test\n", " X_dev[target] = y_dev\n", "\n", " return {\"train\": Xtrain_mod, \"test\": X_test, \"dev\": X_dev}\n" ] }, { "cell_type": "markdown", "id": "analyzed-radical", "metadata": {}, "source": [ "#### Place your custom module in `~/lightwood_modules`\n", "\n", "We automatically search for custom scripts in your `~/lightwood_modules` path. Place your file there. Later, you'll see when we autogenerate code, that you can change your import location if you choose." ] }, { "cell_type": "code", "execution_count": 6, "id": "34092d12", "metadata": { "execution": { "iopub.execute_input": "2024-05-07T17:13:39.940142Z", "iopub.status.busy": "2024-05-07T17:13:39.939757Z", "iopub.status.idle": "2024-05-07T17:13:39.943266Z", "shell.execute_reply": "2024-05-07T17:13:39.942717Z" } }, "outputs": [], "source": [ "from lightwood import load_custom_module\n", "\n", "load_custom_module('MyCustomSplitter.py')" ] }, { "cell_type": "markdown", "id": "lucky-blair", "metadata": {}, "source": [ "### 4) Introduce your custom splitter in JSON-AI\n", "\n", "Now let's introduce our custom splitter. JSON-AI keeps a lightweight syntax but fills in many default modules (like splitting, cleaning).\n", "\n", "For the custom cleaner, we'll work by editing the \"splitter\" key. We will change properties within it as follows:\n", "(1) \"module\" - place the name of the function. In our case it will be \"MyCustomCleaner.cleaner\"\n", "(2) \"args\" - any keyword argument specific to your cleaner's internals. \n", "\n", "This will look as follows:\n", "```\n", " \"splitter\": {\n", " \"module\": \"MyCustomSplitter.MySplitter\",\n", " \"args\": {\n", " \"data\": \"data\",\n", " \"target\": \"$target\",\n", " \"pct_train\": 0.8,\n", " \"pct_dev\": 0.1,\n", " \"seed\": 1\n", " }\n", " },\n", "```" ] }, { "cell_type": "markdown", "id": "identical-georgia", "metadata": {}, "source": [ "### 5) Generate Python code representing your ML pipeline\n", "\n", "Now we're ready to load up our custom JSON-AI and generate the predictor code!\n", "\n", "We can do this by first reading in our custom json-syntax, and then calling the function `code_from_json_ai`. " ] }, { "cell_type": "code", "execution_count": 7, "id": "alleged-concentrate", "metadata": { "execution": { "iopub.execute_input": "2024-05-07T17:13:39.945933Z", "iopub.status.busy": "2024-05-07T17:13:39.945551Z", "iopub.status.idle": "2024-05-07T17:13:40.181125Z", "shell.execute_reply": "2024-05-07T17:13:40.180436Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "import lightwood\n", "from lightwood import __version__ as lightwood_version\n", "from lightwood.analysis import *\n", "from lightwood.api import *\n", "from lightwood.data import *\n", "from lightwood.encoder import *\n", "from lightwood.ensemble import *\n", "from lightwood.helpers.device import *\n", "from lightwood.helpers.general import *\n", "from lightwood.helpers.ts import *\n", "from lightwood.helpers.log import *\n", "from lightwood.helpers.numeric import *\n", "from lightwood.helpers.parallelism import *\n", "from lightwood.helpers.seed import *\n", "from lightwood.helpers.text import *\n", "from lightwood.helpers.torch import *\n", "from lightwood.mixer import *\n", "\n", "from dataprep_ml.insights import statistical_analysis\n", "from dataprep_ml.cleaners import cleaner\n", "from dataprep_ml.splitters import splitter\n", "from dataprep_ml.imputers import *\n", "\n", "from mindsdb_evaluator import evaluate_accuracies\n", "from mindsdb_evaluator.accuracy import __all__ as mdb_eval_accuracy_metrics\n", "\n", "import pandas as pd\n", "from typing import Dict, List, Union, Optional\n", "import os\n", "from types import ModuleType\n", "import importlib.machinery\n", "import sys\n", "import time\n", "\n", "\n", "for import_dir in [\n", " os.path.join(\n", " os.path.expanduser(\"~/lightwood_modules\"), lightwood_version.replace(\".\", \"_\")\n", " ),\n", " os.path.join(\"/etc/lightwood_modules\", lightwood_version.replace(\".\", \"_\")),\n", "]:\n", " if os.path.exists(import_dir) and os.access(import_dir, os.R_OK):\n", " for file_name in list(os.walk(import_dir))[0][2]:\n", " if file_name[-3:] != \".py\":\n", " continue\n", " mod_name = file_name[:-3]\n", " loader = importlib.machinery.SourceFileLoader(\n", " mod_name, os.path.join(import_dir, file_name)\n", " )\n", " module = ModuleType(loader.name)\n", " loader.exec_module(module)\n", " sys.modules[mod_name] = module\n", " exec(f\"import {mod_name}\")\n", "\n", "\n", "class Predictor(PredictorInterface):\n", " target: str\n", " mixers: List[BaseMixer]\n", " encoders: Dict[str, BaseEncoder]\n", " ensemble: BaseEnsemble\n", " mode: str\n", "\n", " def __init__(self):\n", " seed(1)\n", " self.target = \"Class\"\n", " self.mode = \"inactive\"\n", " self.problem_definition = ProblemDefinition.from_dict(\n", " {\n", " \"target\": \"Class\",\n", " \"pct_invalid\": 2,\n", " \"unbias_target\": True,\n", " \"seconds_per_mixer\": 42768.0,\n", " \"seconds_per_encoder\": None,\n", " \"expected_additional_time\": 69.30250954627991,\n", " \"time_aim\": 259200,\n", " \"target_weights\": None,\n", " \"positive_domain\": False,\n", " \"timeseries_settings\": {\n", " \"is_timeseries\": False,\n", " \"order_by\": None,\n", " \"window\": None,\n", " \"group_by\": None,\n", " \"use_previous_target\": True,\n", " \"horizon\": None,\n", " \"historical_columns\": None,\n", " \"target_type\": \"\",\n", " \"allow_incomplete_history\": True,\n", " \"eval_incomplete\": False,\n", " \"interval_periods\": [],\n", " },\n", " \"anomaly_detection\": False,\n", " \"use_default_analysis\": True,\n", " \"embedding_only\": False,\n", " \"dtype_dict\": {},\n", " \"ignore_features\": [],\n", " \"fit_on_all\": True,\n", " \"strict_mode\": True,\n", " \"seed_nr\": 1,\n", " }\n", " )\n", " self.accuracy_functions = [\"balanced_accuracy_score\"]\n", " self.identifiers = {}\n", " self.dtype_dict = {\n", " \"Time\": \"integer\",\n", " \"V1\": \"float\",\n", " \"V2\": \"float\",\n", " \"V3\": \"float\",\n", " \"V4\": \"float\",\n", " \"V5\": \"float\",\n", " \"V6\": \"float\",\n", " \"V7\": \"float\",\n", " \"V8\": \"float\",\n", " \"V9\": \"float\",\n", " \"V10\": \"float\",\n", " \"V11\": \"float\",\n", " \"V12\": \"float\",\n", " \"V13\": \"float\",\n", " \"V14\": \"float\",\n", " \"V15\": \"float\",\n", " \"V16\": \"float\",\n", " \"V17\": \"float\",\n", " \"V18\": \"float\",\n", " \"V19\": \"float\",\n", " \"V20\": \"float\",\n", " \"V21\": \"float\",\n", " \"V22\": \"float\",\n", " \"V23\": \"float\",\n", " \"V24\": \"float\",\n", " \"V25\": \"float\",\n", " \"V26\": \"float\",\n", " \"V27\": \"float\",\n", " \"V28\": \"float\",\n", " \"Amount\": \"float\",\n", " \"Class\": \"binary\",\n", " }\n", " self.lightwood_version = \"24.3.3.1\"\n", " self.pred_args = PredictionArguments()\n", "\n", " # Any feature-column dependencies\n", " self.dependencies = {\n", " \"Class\": [],\n", " \"Time\": [],\n", " \"V1\": [],\n", " \"V2\": [],\n", " \"V3\": [],\n", " \"V4\": [],\n", " \"V5\": [],\n", " \"V6\": [],\n", " \"V7\": [],\n", " \"V8\": [],\n", " \"V9\": [],\n", " \"V10\": [],\n", " \"V11\": [],\n", " \"V12\": [],\n", " \"V13\": [],\n", " \"V14\": [],\n", " \"V15\": [],\n", " \"V16\": [],\n", " \"V17\": [],\n", " \"V18\": [],\n", " \"V19\": [],\n", " \"V20\": [],\n", " \"V21\": [],\n", " \"V22\": [],\n", " \"V23\": [],\n", " \"V24\": [],\n", " \"V25\": [],\n", " \"V26\": [],\n", " \"V27\": [],\n", " \"V28\": [],\n", " \"Amount\": [],\n", " }\n", "\n", " self.input_cols = [\n", " \"Time\",\n", " \"V1\",\n", " \"V2\",\n", " \"V3\",\n", " \"V4\",\n", " \"V5\",\n", " \"V6\",\n", " \"V7\",\n", " \"V8\",\n", " \"V9\",\n", " \"V10\",\n", " \"V11\",\n", " \"V12\",\n", " \"V13\",\n", " \"V14\",\n", " \"V15\",\n", " \"V16\",\n", " \"V17\",\n", " \"V18\",\n", " \"V19\",\n", " \"V20\",\n", " \"V21\",\n", " \"V22\",\n", " \"V23\",\n", " \"V24\",\n", " \"V25\",\n", " \"V26\",\n", " \"V27\",\n", " \"V28\",\n", " \"Amount\",\n", " ]\n", "\n", " # Initial stats analysis\n", " self.statistical_analysis = None\n", " self.ts_analysis = None\n", " self.runtime_log = dict()\n", " self.global_insights = dict()\n", "\n", " # Feature cache\n", " self.feature_cache = dict()\n", "\n", " @timed_predictor\n", " def analyze_data(self, data: pd.DataFrame) -> None:\n", " # Perform a statistical analysis on the unprocessed data\n", "\n", " self.statistical_analysis = statistical_analysis(\n", " data, self.dtype_dict, self.problem_definition.to_dict(), {}\n", " )\n", "\n", " # Instantiate post-training evaluation\n", " self.analysis_blocks = [\n", " ICP(fixed_significance=None, confidence_normalizer=False, deps=[]),\n", " ConfStats(deps=[\"ICP\"]),\n", " AccStats(deps=[\"ICP\"]),\n", " PermutationFeatureImportance(deps=[\"AccStats\"]),\n", " ]\n", "\n", " @timed_predictor\n", " def preprocess(self, data: pd.DataFrame) -> pd.DataFrame:\n", " # Preprocess and clean data\n", "\n", " log.info(\"Cleaning the data\")\n", " self.imputers = {}\n", " data = cleaner(\n", " data=data,\n", " pct_invalid=self.problem_definition.pct_invalid,\n", " identifiers=self.identifiers,\n", " dtype_dict=self.dtype_dict,\n", " target=self.target,\n", " mode=self.mode,\n", " imputers=self.imputers,\n", " timeseries_settings=self.problem_definition.timeseries_settings.to_dict(),\n", " anomaly_detection=self.problem_definition.anomaly_detection,\n", " )\n", "\n", " # Time-series blocks\n", "\n", " return data\n", "\n", " @timed_predictor\n", " def split(self, data: pd.DataFrame) -> Dict[str, pd.DataFrame]:\n", " # Split the data into training/testing splits\n", "\n", " log.info(\"Splitting the data into train/test\")\n", " train_test_data = MyCustomSplitter.MySplitter(\n", " data=data, pct_train=0.8, pct_dev=0.1, seed=1, target=self.target\n", " )\n", "\n", " return train_test_data\n", "\n", " @timed_predictor\n", " def prepare(self, data: Dict[str, pd.DataFrame]) -> None:\n", " # Prepare encoders to featurize data\n", "\n", " self.mode = \"train\"\n", "\n", " if self.statistical_analysis is None:\n", " raise Exception(\"Please run analyze_data first\")\n", "\n", " # Column to encoder mapping\n", " self.encoders = {\n", " \"Class\": BinaryEncoder(\n", " is_target=True, target_weights=self.statistical_analysis.target_weights\n", " ),\n", " \"Time\": NumericEncoder(),\n", " \"V1\": NumericEncoder(),\n", " \"V2\": NumericEncoder(),\n", " \"V3\": NumericEncoder(),\n", " \"V4\": NumericEncoder(),\n", " \"V5\": NumericEncoder(),\n", " \"V6\": NumericEncoder(),\n", " \"V7\": NumericEncoder(),\n", " \"V8\": NumericEncoder(),\n", " \"V9\": NumericEncoder(),\n", " \"V10\": NumericEncoder(),\n", " \"V11\": NumericEncoder(),\n", " \"V12\": NumericEncoder(),\n", " \"V13\": NumericEncoder(),\n", " \"V14\": NumericEncoder(),\n", " \"V15\": NumericEncoder(),\n", " \"V16\": NumericEncoder(),\n", " \"V17\": NumericEncoder(),\n", " \"V18\": NumericEncoder(),\n", " \"V19\": NumericEncoder(),\n", " \"V20\": NumericEncoder(),\n", " \"V21\": NumericEncoder(),\n", " \"V22\": NumericEncoder(),\n", " \"V23\": NumericEncoder(),\n", " \"V24\": NumericEncoder(),\n", " \"V25\": NumericEncoder(),\n", " \"V26\": NumericEncoder(),\n", " \"V27\": NumericEncoder(),\n", " \"V28\": NumericEncoder(),\n", " \"Amount\": NumericEncoder(),\n", " }\n", "\n", " # Prepare the training + dev data\n", " concatenated_train_dev = pd.concat([data[\"train\"], data[\"dev\"]])\n", "\n", " prepped_encoders = {}\n", "\n", " # Prepare input encoders\n", " parallel_encoding = parallel_encoding_check(data[\"train\"], self.encoders)\n", "\n", " if parallel_encoding:\n", " log.debug(\"Preparing in parallel...\")\n", " for col_name, encoder in self.encoders.items():\n", " if col_name != self.target and not encoder.is_trainable_encoder:\n", " prepped_encoders[col_name] = (\n", " encoder,\n", " concatenated_train_dev[col_name],\n", " \"prepare\",\n", " )\n", " prepped_encoders = mut_method_call(prepped_encoders)\n", "\n", " else:\n", " log.debug(\"Preparing sequentially...\")\n", " for col_name, encoder in self.encoders.items():\n", " if col_name != self.target and not encoder.is_trainable_encoder:\n", " log.debug(f\"Preparing encoder for {col_name}...\")\n", " encoder.prepare(concatenated_train_dev[col_name])\n", " prepped_encoders[col_name] = encoder\n", "\n", " # Store encoders\n", " for col_name, encoder in prepped_encoders.items():\n", " self.encoders[col_name] = encoder\n", "\n", " # Prepare the target\n", " if self.target not in prepped_encoders:\n", " if self.encoders[self.target].is_trainable_encoder:\n", " self.encoders[self.target].prepare(\n", " data[\"train\"][self.target], data[\"dev\"][self.target]\n", " )\n", " else:\n", " self.encoders[self.target].prepare(\n", " pd.concat([data[\"train\"], data[\"dev\"]])[self.target]\n", " )\n", "\n", " # Prepare any non-target encoders that are learned\n", " for col_name, encoder in self.encoders.items():\n", " if col_name != self.target and encoder.is_trainable_encoder:\n", " priming_data = pd.concat([data[\"train\"], data[\"dev\"]])\n", " kwargs = {}\n", " if self.dependencies[col_name]:\n", " kwargs[\"dependency_data\"] = {}\n", " for col in self.dependencies[col_name]:\n", " kwargs[\"dependency_data\"][col] = {\n", " \"original_type\": self.dtype_dict[col],\n", " \"data\": priming_data[col],\n", " }\n", "\n", " # If an encoder representation requires the target, provide priming data\n", " if hasattr(encoder, \"uses_target\"):\n", " kwargs[\"encoded_target_values\"] = self.encoders[self.target].encode(\n", " priming_data[self.target]\n", " )\n", "\n", " encoder.prepare(\n", " data[\"train\"][col_name], data[\"dev\"][col_name], **kwargs\n", " )\n", "\n", " @timed_predictor\n", " def featurize(self, split_data: Dict[str, pd.DataFrame]):\n", " # Featurize data into numerical representations for models\n", "\n", " log.info(\"Featurizing the data\")\n", "\n", " tss = self.problem_definition.timeseries_settings\n", "\n", " feature_data = dict()\n", " for key, data in split_data.items():\n", " if key != \"stratified_on\":\n", "\n", " # compute and store two splits - full and filtered (useful for time series post-train analysis)\n", " if key not in self.feature_cache:\n", " featurized_split = EncodedDs(self.encoders, data, self.target)\n", " filtered_subset = EncodedDs(\n", " self.encoders, filter_ts(data, tss), self.target\n", " )\n", "\n", " for k, s in zip(\n", " (key, f\"{key}_filtered\"), (featurized_split, filtered_subset)\n", " ):\n", " self.feature_cache[k] = s\n", "\n", " for k in (key, f\"{key}_filtered\"):\n", " feature_data[k] = self.feature_cache[k]\n", "\n", " return feature_data\n", "\n", " @timed_predictor\n", " def fit(self, enc_data: Dict[str, pd.DataFrame]) -> None:\n", " # Fit predictors to estimate target\n", "\n", " self.mode = \"train\"\n", "\n", " # --------------- #\n", " # Extract data\n", " # --------------- #\n", " # Extract the featurized data into train/dev/test\n", " encoded_train_data = enc_data[\"train\"]\n", " encoded_dev_data = enc_data[\"dev\"]\n", " encoded_test_data = enc_data[\"test_filtered\"]\n", "\n", " log.info(\"Training the mixers\")\n", "\n", " # --------------- #\n", " # Fit Models\n", " # --------------- #\n", " # Assign list of mixers\n", " self.mixers = [\n", " Neural(\n", " fit_on_dev=True,\n", " search_hyperparameters=True,\n", " net=\"DefaultNet\",\n", " stop_after=self.problem_definition.seconds_per_mixer,\n", " target=self.target,\n", " dtype_dict=self.dtype_dict,\n", " target_encoder=self.encoders[self.target],\n", " ),\n", " XGBoostMixer(\n", " fit_on_dev=True,\n", " use_optuna=True,\n", " stop_after=self.problem_definition.seconds_per_mixer,\n", " target=self.target,\n", " dtype_dict=self.dtype_dict,\n", " input_cols=self.input_cols,\n", " target_encoder=self.encoders[self.target],\n", " ),\n", " Regression(\n", " stop_after=self.problem_definition.seconds_per_mixer,\n", " target=self.target,\n", " dtype_dict=self.dtype_dict,\n", " target_encoder=self.encoders[self.target],\n", " ),\n", " RandomForest(\n", " fit_on_dev=True,\n", " stop_after=self.problem_definition.seconds_per_mixer,\n", " target=self.target,\n", " dtype_dict=self.dtype_dict,\n", " target_encoder=self.encoders[self.target],\n", " ),\n", " ]\n", "\n", " # Train mixers\n", " trained_mixers = []\n", " for mixer in self.mixers:\n", " try:\n", " if mixer.trains_once:\n", " self.fit_mixer(\n", " mixer,\n", " ConcatedEncodedDs([encoded_train_data, encoded_dev_data]),\n", " encoded_test_data,\n", " )\n", " else:\n", " self.fit_mixer(mixer, encoded_train_data, encoded_dev_data)\n", " trained_mixers.append(mixer)\n", " except Exception as e:\n", " log.warning(f\"Exception: {e} when training mixer: {mixer}\")\n", " if True and mixer.stable:\n", " raise e\n", "\n", " # Update mixers to trained versions\n", " if not trained_mixers:\n", " raise Exception(\n", " \"No mixers could be trained! Please verify your problem definition or JsonAI model representation.\"\n", " )\n", " self.mixers = trained_mixers\n", "\n", " # --------------- #\n", " # Create Ensembles\n", " # --------------- #\n", " log.info(\"Ensembling the mixer\")\n", " # Create an ensemble of mixers to identify best performing model\n", " # Dirty hack\n", " self.ensemble = BestOf(\n", " data=encoded_test_data,\n", " fit=True,\n", " ts_analysis=None,\n", " target=self.target,\n", " mixers=self.mixers,\n", " args=self.pred_args,\n", " accuracy_functions=self.accuracy_functions,\n", " )\n", " self.supports_proba = self.ensemble.supports_proba\n", "\n", " @timed_predictor\n", " def fit_mixer(self, mixer, encoded_train_data, encoded_dev_data) -> None:\n", " mixer.fit(encoded_train_data, encoded_dev_data)\n", "\n", " @timed_predictor\n", " def analyze_ensemble(self, enc_data: Dict[str, pd.DataFrame]) -> None:\n", " # Evaluate quality of fit for the ensemble of mixers\n", "\n", " # --------------- #\n", " # Extract data\n", " # --------------- #\n", " # Extract the featurized data into train/dev/test\n", " encoded_train_data = enc_data[\"train\"]\n", " encoded_dev_data = enc_data[\"dev\"]\n", " encoded_test_data = enc_data[\"test\"]\n", "\n", " # --------------- #\n", " # Analyze Ensembles\n", " # --------------- #\n", " log.info(\"Analyzing the ensemble of mixers\")\n", " self.model_analysis, self.runtime_analyzer = model_analyzer(\n", " data=encoded_test_data,\n", " train_data=encoded_train_data,\n", " ts_analysis=None,\n", " stats_info=self.statistical_analysis,\n", " pdef=self.problem_definition,\n", " accuracy_functions=self.accuracy_functions,\n", " predictor=self.ensemble,\n", " target=self.target,\n", " dtype_dict=self.dtype_dict,\n", " analysis_blocks=self.analysis_blocks,\n", " )\n", "\n", " @timed_predictor\n", " def learn(self, data: pd.DataFrame) -> None:\n", " if self.problem_definition.ignore_features:\n", " log.info(f\"Dropping features: {self.problem_definition.ignore_features}\")\n", " data = data.drop(\n", " columns=self.problem_definition.ignore_features, errors=\"ignore\"\n", " )\n", "\n", " self.mode = \"train\"\n", " n_phases = 8 if self.problem_definition.fit_on_all else 7\n", "\n", " # Perform stats analysis\n", " log.info(f\"[Learn phase 1/{n_phases}] - Statistical analysis\")\n", " self.analyze_data(data)\n", "\n", " # Pre-process the data\n", " log.info(f\"[Learn phase 2/{n_phases}] - Data preprocessing\")\n", " data = self.preprocess(data)\n", "\n", " # Create train/test (dev) split\n", " log.info(f\"[Learn phase 3/{n_phases}] - Data splitting\")\n", " train_dev_test = self.split(data)\n", "\n", " # Prepare encoders\n", " log.info(f\"[Learn phase 4/{n_phases}] - Preparing encoders\")\n", " self.prepare(train_dev_test)\n", "\n", " # Create feature vectors from data\n", " log.info(f\"[Learn phase 5/{n_phases}] - Feature generation\")\n", " enc_train_test = self.featurize(train_dev_test)\n", "\n", " # Prepare mixers\n", " log.info(f\"[Learn phase 6/{n_phases}] - Mixer training\")\n", " if not self.problem_definition.embedding_only:\n", " self.fit(enc_train_test)\n", " else:\n", " self.mixers = []\n", " self.ensemble = Embedder(\n", " self.target, mixers=list(), data=enc_train_test[\"train\"]\n", " )\n", " self.supports_proba = self.ensemble.supports_proba\n", "\n", " # Analyze the ensemble\n", " log.info(f\"[Learn phase 7/{n_phases}] - Ensemble analysis\")\n", " self.analyze_ensemble(enc_train_test)\n", "\n", " # ------------------------ #\n", " # Enable model partial fit AFTER it is trained and evaluated for performance with the appropriate train/dev/test splits.\n", " # This assumes the predictor could continuously evolve, hence including reserved testing data may improve predictions.\n", " # SET `json_ai.problem_definition.fit_on_all=False` TO TURN THIS BLOCK OFF.\n", "\n", " # Update the mixers with partial fit\n", " if self.problem_definition.fit_on_all and all(\n", " [not m.trains_once for m in self.mixers]\n", " ):\n", " log.info(f\"[Learn phase 8/{n_phases}] - Adjustment on validation requested\")\n", " self.adjust(\n", " enc_train_test[\"test\"].data_frame,\n", " ConcatedEncodedDs(\n", " [enc_train_test[\"train\"], enc_train_test[\"dev\"]]\n", " ).data_frame,\n", " adjust_args={\"learn_call\": True},\n", " )\n", "\n", " self.feature_cache = (\n", " dict()\n", " ) # empty feature cache to avoid large predictor objects\n", "\n", " @timed_predictor\n", " def adjust(\n", " self,\n", " train_data: Union[EncodedDs, ConcatedEncodedDs, pd.DataFrame],\n", " dev_data: Optional[Union[EncodedDs, ConcatedEncodedDs, pd.DataFrame]] = None,\n", " adjust_args: Optional[dict] = None,\n", " ) -> None:\n", " # Update mixers with new information\n", "\n", " self.mode = \"train\"\n", "\n", " # --------------- #\n", " # Prepare data\n", " # --------------- #\n", " if dev_data is None:\n", " data = train_data\n", " split = splitter(\n", " data=data,\n", " pct_train=0.8,\n", " pct_dev=0.2,\n", " pct_test=0,\n", " tss=self.problem_definition.timeseries_settings.to_dict(),\n", " seed=self.problem_definition.seed_nr,\n", " target=self.target,\n", " dtype_dict=self.dtype_dict,\n", " )\n", " train_data = split[\"train\"]\n", " dev_data = split[\"dev\"]\n", "\n", " if adjust_args is None or not adjust_args.get(\"learn_call\"):\n", " train_data = self.preprocess(train_data)\n", " dev_data = self.preprocess(dev_data)\n", "\n", " dev_data = EncodedDs(self.encoders, dev_data, self.target)\n", " train_data = EncodedDs(self.encoders, train_data, self.target)\n", "\n", " # --------------- #\n", " # Update/Adjust Mixers\n", " # --------------- #\n", " log.info(\"Updating the mixers\")\n", "\n", " for mixer in self.mixers:\n", " mixer.partial_fit(train_data, dev_data, adjust_args)\n", "\n", " @timed_predictor\n", " def predict(self, data: pd.DataFrame, args: Dict = {}) -> pd.DataFrame:\n", "\n", " self.mode = \"predict\"\n", " n_phases = 3 if self.pred_args.all_mixers else 4\n", "\n", " if len(data) == 0:\n", " raise Exception(\n", " \"Empty input, aborting prediction. Please try again with some input data.\"\n", " )\n", "\n", " self.pred_args = PredictionArguments.from_dict(args)\n", "\n", " log.info(f\"[Predict phase 1/{n_phases}] - Data preprocessing\")\n", " if self.problem_definition.ignore_features:\n", " log.info(f\"Dropping features: {self.problem_definition.ignore_features}\")\n", " data = data.drop(\n", " columns=self.problem_definition.ignore_features, errors=\"ignore\"\n", " )\n", " for col in self.input_cols:\n", " if col not in data.columns:\n", " data[col] = [None] * len(data)\n", "\n", " # Pre-process the data\n", " data = self.preprocess(data)\n", "\n", " # Featurize the data\n", " log.info(f\"[Predict phase 2/{n_phases}] - Feature generation\")\n", " encoded_ds = self.featurize({\"predict_data\": data})[\"predict_data\"]\n", " encoded_data = encoded_ds.get_encoded_data(include_target=False)\n", "\n", " log.info(f\"[Predict phase 3/{n_phases}] - Calling ensemble\")\n", "\n", " @timed\n", " def _timed_call(encoded_ds):\n", " if self.pred_args.return_embedding:\n", " embedder = Embedder(self.target, mixers=list(), data=encoded_ds)\n", " df = embedder(encoded_ds, args=self.pred_args)\n", " else:\n", " df = self.ensemble(encoded_ds, args=self.pred_args)\n", " return df\n", "\n", " df = _timed_call(encoded_ds)\n", "\n", " if not (\n", " any(\n", " [\n", " self.pred_args.all_mixers,\n", " self.pred_args.return_embedding,\n", " self.problem_definition.embedding_only,\n", " ]\n", " )\n", " ):\n", " log.info(f\"[Predict phase 4/{n_phases}] - Analyzing output\")\n", " df, global_insights = explain(\n", " data=data,\n", " encoded_data=encoded_data,\n", " predictions=df,\n", " ts_analysis=None,\n", " problem_definition=self.problem_definition,\n", " stat_analysis=self.statistical_analysis,\n", " runtime_analysis=self.runtime_analyzer,\n", " target_name=self.target,\n", " target_dtype=self.dtype_dict[self.target],\n", " explainer_blocks=self.analysis_blocks,\n", " pred_args=self.pred_args,\n", " )\n", " self.global_insights = {**self.global_insights, **global_insights}\n", "\n", " self.feature_cache = (\n", " dict()\n", " ) # empty feature cache to avoid large predictor objects\n", "\n", " return df\n", "\n", " def test(\n", " self,\n", " data: pd.DataFrame,\n", " metrics: list,\n", " args: Dict[str, object] = {},\n", " strict: bool = False,\n", " ) -> pd.DataFrame:\n", "\n", " preds = self.predict(data, args)\n", " preds = preds.rename(columns={\"prediction\": self.target})\n", " filtered = []\n", "\n", " # filter metrics if not supported\n", " for metric in metrics:\n", " # metric should be one of: an actual function, registered in the model class, or supported by the evaluator\n", " if not (\n", " callable(metric)\n", " or metric in self.accuracy_functions\n", " or metric in mdb_eval_accuracy_metrics\n", " ):\n", " if strict:\n", " raise Exception(f\"Invalid metric: {metric}\")\n", " else:\n", " log.warning(f\"Invalid metric: {metric}. Skipping...\")\n", " else:\n", " filtered.append(metric)\n", "\n", " metrics = filtered\n", " try:\n", " labels = self.model_analysis.histograms[self.target][\"x\"]\n", " except:\n", " if strict:\n", " raise Exception(\"Label histogram not found\")\n", " else:\n", " label_map = (\n", " None # some accuracy functions will crash without this, be mindful\n", " )\n", " scores = evaluate_accuracies(\n", " data,\n", " preds[self.target],\n", " self.target,\n", " metrics,\n", " ts_analysis=self.ts_analysis,\n", " labels=labels,\n", " )\n", "\n", " # TODO: remove once mdb_eval returns an actual list\n", " scores = {k: [v] for k, v in scores.items() if not isinstance(v, list)}\n", "\n", " return pd.DataFrame.from_records(\n", " scores\n", " ) # TODO: add logic to disaggregate per-mixer\n", "\n" ] } ], "source": [ "json_ai.splitter = {\n", " \"module\": \"MyCustomSplitter.MySplitter\",\n", " \"args\": {\n", " \"data\": \"data\",\n", " \"target\": \"$target\",\n", " \"pct_train\": 0.8,\n", " \"pct_dev\": 0.1,\n", " \"seed\": 1\n", " }\n", " }\n", "\n", "#Generate python code that fills in your pipeline\n", "code = code_from_json_ai(json_ai)\n", "\n", "print(code)\n", "\n", "# Save code to a file (Optional)\n", "with open('custom_splitter_pipeline.py', 'w') as fp:\n", " fp.write(code)" ] }, { "cell_type": "markdown", "id": "dental-beauty", "metadata": {}, "source": [ "As you can see, an end-to-end pipeline of our entire ML procedure has been generating. There are several abstracted functions to enable transparency as to what processes your data goes through in order to build these models.\n", "\n", "The key steps of the pipeline are as follows:\n", "\n", "(1) Run a **statistical analysis** with `analyze_data`
\n", "(2) Clean your data with `preprocess`
\n", "(3) Make a training/dev/testing split with `split`
\n", "(4) Prepare your feature-engineering pipelines with `prepare`
\n", "(5) Create your features with `featurize`
\n", "(6) Fit your predictor models with `fit`
\n", "\n", "You can customize this further if necessary, but you have all the steps necessary to train a model!\n", "\n", "We recommend familiarizing with these steps by calling the above commands, ideally in order. Some commands (namely `prepare`, `featurize`, and `fit`) do depend on other steps.\n", "\n", "If you want to omit the individual steps, we recommend your simply call the `learn` method, which compiles all the necessary steps implemented to give your fully trained predictive models starting with unprocessed data! " ] }, { "cell_type": "markdown", "id": "amended-oklahoma", "metadata": {}, "source": [ "### 6) Call python to run your code and see your preprocessed outputs\n", "\n", "Once we have code, we can turn this into a python object by calling `predictor_from_code`. This instantiates the `PredictorInterface` object. \n", "\n", "This predictor object can be then used to run your pipeline." ] }, { "cell_type": "code", "execution_count": 8, "id": "organic-london", "metadata": { "execution": { "iopub.execute_input": "2024-05-07T17:13:40.183770Z", "iopub.status.busy": "2024-05-07T17:13:40.183572Z", "iopub.status.idle": "2024-05-07T17:13:40.190941Z", "shell.execute_reply": "2024-05-07T17:13:40.190412Z" } }, "outputs": [], "source": [ "# Turn the code above into a predictor object\n", "predictor = predictor_from_code(code)" ] }, { "cell_type": "code", "execution_count": 9, "id": "fabulous-prime", "metadata": { "execution": { "iopub.execute_input": "2024-05-07T17:13:40.193217Z", "iopub.status.busy": "2024-05-07T17:13:40.192988Z", "iopub.status.idle": "2024-05-07T17:14:00.395495Z", "shell.execute_reply": "2024-05-07T17:14:00.394798Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:dataprep_ml-2306:Cleaning the data\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[37mDEBUG:lightwood-2306: `preprocess` runtime: 18.64 seconds\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32mINFO:dataprep_ml-2306:Splitting the data into train/test\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\u001b[37mDEBUG:lightwood-2306: `split` runtime: 1.55 seconds\u001b[0m\n" ] } ], "source": [ "# Pre-process the data\n", "cleaned_data = predictor.preprocess(data)\n", "train_test_data = predictor.split(cleaned_data)" ] }, { "cell_type": "code", "execution_count": 10, "id": "suspended-biography", "metadata": { "execution": { "iopub.execute_input": "2024-05-07T17:14:00.398126Z", "iopub.status.busy": "2024-05-07T17:14:00.397903Z", "iopub.status.idle": "2024-05-07T17:14:01.928878Z", "shell.execute_reply": "2024-05-07T17:14:01.928158Z" } }, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAABu8AAAHbCAYAAADLWQf1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAB3Z0lEQVR4nOzdeXRU9f3/8deEkISQjbBDCGFVouxGiYigaEX4IoqIW1GKbcGGVoWvCxUQDApuaC3Tr20VRBRFsbKIUqQssiUGEKJGpCgJi0CDkAlLSCDc3x/5ZUrMwtzJTObOzPNxTo7M/dzlfUdy74t537nXZhiGIQAAAAAAAAAAAAA+F+LrAgAAAAAAAAAAAACUoXkHAAAAAAAAAAAAWATNOwAAAAAAAAAAAMAiaN4BAAAAAAAAAAAAFkHzDgAAAAAAAAAAALAImncAAAAAAAAAAACARdC8AwAAAAAAAAAAACyC5h0AAAAAAAAAAABgETTvAAAAAAAAAAAAAIugeQcAblq3bp1sNptsNpuvSwEAAAAAAAAABAiadwAsrbw55s7Pm2++6evyAQAALGnatGmVslNISIhiYmKUkJCgq6++WmlpaVq8eLFKSkp8XS4AAIBP+MPnUgUFBZo2bZqmTZumgoKCOtkmAO8L9XUBAFCT5s2bVzn95MmTOnXqVI3zNGjQwGt1SVJkZKQuueQSr24DAADA2y7MUkVFRfrxxx918OBBbdmyRX/5y1/UuHFjzZgxQ+PGjfNhlQAAAHXPyp9LlSsoKND06dMlSaNHj1ZcXFydbBeAd9G8A2Bphw8frnL6tGnTnMGkunm87corr9SuXbt8sm0AAABP+XmWKi0tVU5Ojj777DPNmTNHe/fu1YMPPqgNGzbo7bff5pbhAAAgaFj5cykAgY3bZgIAAAAAnOrVq6euXbtqwoQJ+vrrr3XXXXdJkhYuXKhZs2b5uDoAAAAACHw07wAEpPL7i69bt07/+c9/NGHCBHXu3FmRkZEVrhY/ffq03n33Xd13333q0aOHmjZtqvDwcLVq1Uq33nqrPv3002q3sW7dOud2fu7NN9+UzWZTUlKSJGnbtm0aOXKkWrZsqfDwcLVv314TJkzQ8ePHPb7vAAAAnhIZGan58+erZ8+ekqRZs2bp2LFjleYrKSnRX/7yF1133XVq0qSJwsLC1KJFCw0bNqzKPPXRRx/JZrMpLCxMP/30U401XHvttbLZbHrggQc8s1MAAAB1ID8/X5MnT1bPnj0VGxuriIgItW/fXg888IC++eabapc7cOCAHnnkEV122WVq2LCh83Oq3r1765FHHlFWVpZz3gEDBqhdu3bO1+3atavw3L0BAwZ4cxcBeBHNOwABbc+ePerWrZtefvll7d+/X6GhFe8W/P777+uee+7RggULlJ2drbNnzyo0NFSHDh3S0qVLNXjwYP3v//5vrWpYuHChUlNT9cEHH6ioqEjnzp3T3r179fLLL6tfv346efJklcslJSURtAAAgM+FhYXpj3/8oySpsLBQS5YsqTCel5enXr16KS0tTevWrdOxY8cUGRmpI0eOaNmyZRo8eLAefPDBCssMGTJE8fHxOnv2rN57771qt52bm6uNGzdKku677z7n9PIPpEaPHu2ZnQQAAPCg1atXq3PnznrmmWe0Y8cOFRUVKTQ0VHv37tXcuXPVq1cvvfXWW5WW27lzp7p166ZXXnlFOTk5Ki4uVsOGDXX48GFt375dr7zyiux2u3P++Ph4NWnSxPm6SZMmat68ufMnPj6+wvoHDBhQ4WJzANZF8w5AQHvkkUcUFxenf/3rXzp16pQKCwv13XffOccbNWqk//3f/9XGjRt18uRJFRQU6NSpU/rxxx81ffp01a9fXy+99JKWLVvm1vbz8/M1ZswY3X///dq3b58KCgp04sQJzZkzR/Xr19c333yj559/3lO7CwAA4BWDBg1SvXr1JEnr1693Tj916pQGDRqkb775RgMGDNC6detUVFSkgoICFRQUaPbs2YqKitJrr72mP/3pT87lwsLCdOedd0qSFixYUO123377bRmGoaSkJF177bVe2jsAAADP+eqrr3TLLbeooKBAv/nNb5STk6OioiKdPHlSeXl5+t3vfqeSkhI98MAD2rp1a4VlJ06cqOPHj6tXr17asmWLzp49q2PHjunMmTPavXu3XnzxRV122WXO+f/xj39U+CZeVlaWDh8+7Pz5xz/+UWf7DcCzaN4BCGghISFavXq1rr/+eoWElB3yOnfu7BwfNmyYXnjhBfXt21eRkZHO6S1bttTUqVP17LPPSpJeffVVt7Z/+vRp3XXXXfr73/+uNm3aSCq7/VRaWpp+//vfS5Leffddt9YNAABQV6KiotS+fXtJ0vfff++cPnv2bO3atUv9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atSokW655Ra9/fbblf4f/fjjj84/HzlypMafchde3fboo49q5MiROnv2rP7+97/r5ptvVlxcnLp27apHH31U3333ncfeSwAIROQn8hP5qTLyE/kJAC6GDEWGIkNVRoYiQwUDmneASSdPntQPP/wgqeyA76rExERt375dK1eu1B/+8Af17t1b58+f16ZNm/TYY4+pY8eOWrNmTa1qq+pr+kDbtm0VFRUlSfryyy+9so3PPvvMeVXT7373O3311VcqLi7WsWPHdPjwYR0+fFiPPPKIJFV5ZdK9996rvLw8vfbaa7rzzjvVpk0b5efn6/3339ett96q/v37V7rC6IYbbtDevXv11ltv6f7771enTp3kcDi0fPlyjRo1Sj179tTBgwed8194O4SioiIZZc99rfFnwIABzmXq16+vRYsWaceOHZo6daquv/56RUZG6uuvv9aLL76oyy67TC+99JIn31YACBjkJ/gb8lMZ8hMA+BYZCv6GDFWGDAVPoHkHmLRy5UrnAfjCg6orQkJCdNNNN+lPf/qTtm7dqmPHjumdd95RYmKijh8/rnvuucfjtzFwx4Unm+rGQkNDK9y7uvyKoPKvhFfF4XB4qMLKyq/4Onr0qIqLi6udr/zr8p66QswVF27r51/Xv1D52M/f29oKDQ3VtddeK6ks4Lh6f24zyu8/f9NNN8lut+vyyy+vFOQPHz5c4zri4+M1duxYvffee9q3b5/27NmjJ554QjabTRs2bNC0adMqLdOwYUONGjVKb775pnbv3q0DBw7oueeeU0REhPNqqHItWrRw/vnCWxGY1b17d02fPl3/+te/VFBQoNWrV+vaa69VaWmpHn30Ue3cudPtdQNAoCI/kZ/MIj+VIT8BQHAjQ5GhzCJDlSFDIRDQvANMKCkp0bPPPitJio2N1a233lqr9UVHR+uee+7RG2+8Iansa9RfffWVc7z8AcNS1VeKeMvatWsvOtatWzfVr1/fOb1Ro0aSyr6eX11wyczMrHa95fvq7n5eccUVksq+7r9+/fpq51u9erUkKSUlxa3tuKNXr17O/fvXv/5V7XzltXXv3r3Ce+sJaWlpksrC6+zZs11e7vz58y7Nt3//fkllDz2uimEYpq/q69Chg2bOnKl77rlHUlnou5jWrVvrscce08SJEystk5KSorCwMEnS8uXLTdVSndDQUA0cOFArVqxQeHi4DMNw/n8EAJQhP5Gf3EF+Ij8BQLAjQ5Gh3EGGIkMhcNC8A1xUVFSk0aNHO7/yPWnSJMXFxbm07MWuZGrQoIHzzxeGpZiYGOefCwoKXC+2ll577TUdPXq00vTvvvtOixcvliTdeeedFca6d+8uqewE+dFHH1VatqioSC+//HK12yzfV3f3s1u3bkpOTpYkzZgxo8LX08t98sknzvB29913u7Udd8TFxemmm26SJL3wwgsV7mFdbufOnfrwww+9VtvgwYP1i1/8QpL09NNPO/8/1uRvf/ub/v73v7u0/tjYWEmq9oqf1157zXmrj5+r6So16b+/Hxf+brizTMOGDZ0h7LnnntO+fftqXMfP781e0zbDw8OdV3lduE0ACHbkJ/KTu8hP5CcACGZkKDKUu8hQZCgEDv7vAjU4f/68vv76a82ePVuXXXaZ3n33XUnSqFGj9Nhjj7m8ns2bN6tbt256+eWX9e233zqvJDEMQ5s3b9aDDz4oqexhsd26dXMu17lzZ+dVGq+//nqdXfl09uxZ3XjjjcrKynLWuXr1at10000qLi5WmzZtNG7cuArLJCQk6JprrpEkTZgwQatXr3aGl23btumGG26o9gG9knT55ZdLkgoLC/X++++7Vfdzzz0nSdqwYYNGjBihvXv3OvfnnXfecQaSq6++utZXrJk1Y8YM1a9fX3v27NFNN93kvLrt/Pnz+uSTTzR48GCdO3dOHTp00NixY71Sw8KFC9WlSxedO3dOI0eO1L333qsNGzZUeKjuoUOHNH/+fPXu3Vtjx45VUVGRS+seNGiQJOnTTz9Venq687YIBQUFevbZZ/X73/++2odrjx8/XiNHjtSHH35Y4e/IyZMn9dprr+mtt96SJA0ZMsQ59txzz+nmm2/WggULKtwGori4WO+//75eeOGFSstI0rPPPqtWrVrp6NGjSk1N1YIFC3TixAnneH5+vj788EPddtttlQJs27ZtNWnSJGVkZFQIUXv27NG9996r06dPO29LAgDBjPxEfvIU8hP5CQCCCRmKDOUpZCgyFAKEAaOkpMRIT0832rVrZ4SFhRlt27Y1nn32WV+XhTry1FNPGZIMSUbz5s2dP3FxcUZISIhzTJLRpEkT47XXXqt2XWvXrnXOW910SUb9+vWNxo0bG6Ghoc5pMTExxueff15pnQ888IBznsjISCMxMdFo27atMXHixEr70L9//4vub/m61q5dW2H63r17nWPvvfeeER0dbUgyoqKijMjISOdYXFyckZWVVeW6v/zySyMmJsY5b0REhNGwYUPne7tixQrn2N69eystP3DgQOd4dHS00bZtW6Nt27bGyy+/7PK+zp4927DZbBXqDQsLc77u2rWrcfDgwUrLzZs3z5BktG3bttr37sL3qKr6L+a9996rUEtMTIwRERHhfN2mTRsjJyenymX79+9vSDKeeuop09u9kMPhMEaOHFnhPbLZbEajRo0q1CLJ6NKli7F169YKy7dt29aQZMybN6/C9JKSEqNfv36V1ln+OzRkyBBj8uTJVf6/u//++ytsNyoqyoiLi6sw7ZprrjFOnjzpXObC31tJRoMGDYz4+PgK+9WlSxfj0KFDld6DnJwco3Pnzs75QkJCjPj4eOff1fKfG264ocJyF46FhIRUes9sNluFv6tAoCM/BTfyUxnyE/mJ/ER+AswiQwU3MlQZMhQZigxFhsLFlT3dM8iNGjVKmzZt0lNPPaWOHTtq7969OnLkiK/Lgg+U/3+32Wxq2LChWrRoocTERPXs2VMDBw7U0KFDnVchmZGSkqL3339fa9eu1RdffKEff/xRR48eVUREhDp27Khf/OIXeuihh9SqVatKy9rtdrVp00Yffvihvv/+e+fXrKu6pYCnXHXVVdq6daueeeYZ/etf/1J+fr5at26twYMHa+rUqUpISKhyuR49eigzM1PTp0/XmjVrVFBQoObNm2v06NGaPHlyjQ8SlqTFixfr6aef1ooVK7Rv3z7nA13N3MbgkUceUf/+/fXyyy9r/fr1OnLkiBo0aKBevXrpzjvv1IMPPqjw8HCX1+dJd955p3r37q0XX3xRq1ev1oEDB1S/fn316NFDt912mx5++OEKt6nwhpiYGC1atEh//OMf9fbbb2vdunXKy8vT8ePH1aBBA7Vr105XXnml7rjjDg0aNKjSA3+rU79+fa1atUqzZs3Su+++q9zcXBmGoSuvvFL333+/fvvb3+rpp5+uctkpU6aod+/eWrt2rb799lsdPnxYJ0+eVLNmzdS9e3fdfffduu+++yrU8tvf/latW7fW2rVr9dVXX+nQoUNyOBxq1KiRLrvsMt1+++0aO3asIiIiKm2vS5cuys7O1vz58/Xhhx9qx44dOnbsmMLCwtSxY0f17NlTN954o0aMGFFhuVWrVmnt2rXauHGj9u3b5zxedOzYUf369VNaWpp69+7t6v8KwO+Rn1CO/FSG/OQd5CfyExBoyFAoR4YqQ4byDjIUGQr+z2YYdfgEUgtasWKFbrvtNmVnZ+vSSy/1dTkAAACWR34CAAAwjwwFAABcFfTNuzvuuEMnTpzQypUrfV0KAACAXyA/AQAAmEeGAgAArgrxdQE12bZtm2bNmqXhw4crISFBNptNNpvtossVFRVp6tSp6ty5syIiItSqVSuNGTNGBw8erDTvF198oU6dOul3v/udoqKiFB0drXvvvVfHjx/3xi4BAAB4FfkJAADAPDIUAACwEkt/8+7WW2/V0qVLK02vqeQzZ87ouuuuU0ZGhlq2bKl+/fopNzdXX3zxhZo2baqMjAy1b9/eOX94eLjCwsLUo0cPPfnkkzp69KgmTpyo1NRULVmyxBu7BQAA4DXkJwAAAPPIUAAAwEpCfV1ATVJTU9WtWzelpKQoJSVFSUlJKi4urnGZGTNmKCMjQ6mpqVq1apWioqIkSbNnz9bEiRM1ZswYrVu3zjn/+fPnZRiGlixZosaNG0uSIiIidMcdd+jf//63OnXq5LX9AwAA8DTyEwAAgHlkKAAAYCWW/ubdz0VERKi4uLjaq55KSkrUrFkzORwObd++XT179qww3r17d2VnZ2vr1q3q3bu3JKlZs2bq0KGDtmzZ4pzv6NGjatq0qZYuXapbbrnFezsEAADgZeQnAAAA88hQAADAlyz9zDuzNm3aJIfDoQ4dOlQKTZI0YsQISdLy5cud07p06VJtEAsJCai3BwAAoBLyEwAAgHlkKAAA4E2Wvm2mWTt37pQk9erVq8rx8unZ2dnOaYMHD9bTTz+to0ePqkmTJpKkNWvWyGaz6fLLL69yPcXFxRVunXD+/HkdO3ZMjRs3dulhxgAAwDoMw9CJEyfUqlWroPzQpK7yk0SGAgAgUAR7fpL4DAoAAJhnJkMFVPNu3759kqSEhIQqx8un5+XlOaeNHTtWr776qoYNG6ZJkybp6NGjeuyxx/TLX/5SSUlJVa5n5syZmj59umeLBwAAPrV///5qM0Qgq6v8JJGhAAAINMGanyQ+gwIAAO5zJUMFVPPu5MmTkqTIyMgqxxs2bChJOnHihHNaXFyc1qxZo/Hjx2vkyJGKjIzUyJEj9eKLL1a7nUmTJmnChAnO1w6HQ4mJidq/f79iYmI8sSsAAKCOFBYWqk2bNoqOjvZ1KT5RV/lJIkMBABAogj0/SXwGBQAAzDOToQKqeeeuSy65RJ999pnL84eHhys8PLzS9JiYGIITAAB+itsOmWM2P0lkKAAAAg35yTw+gwIAAK5kqIC6MXlUVJQk6fTp01WOnzp1SpI8dmWY3W5XcnKyUlJSPLI+AACAulbX+UkiQwEAAP/HZ1AAAMCbAqp5l5iYKEk6cOBAlePl09u2beuR7aWlpSknJ0dZWVkeWR8AAEBdq+v8JJGhAACA/+MzKAAA4E0B1bzr3r27JGn79u1VjpdP79atW53VBAAAYGXkJwAAAPPIUAAAwJsCqnnXt29fxcbG6vvvv9eOHTsqjS9evFiSNHToUI9sj1sWAAAAf1fX+UkiQwEAAP/HZ1AAAMCbAqp5FxYWpvHjx0squ51A+f3FJWn27NnKzs5W//791bt3b49sj1sWAAAAf1fX+al8O2QoAADgz/gMCgAAeFOorwuoyYoVK5Senu58XVJSIknq06ePc9qUKVM0ZMgQ5+vJkydr9erV2rx5szp16qR+/fopLy9PmZmZatq0qebOnVt3OwAAAFDHyE8AAADmkaEAAICVWLp5l5+fr8zMzErTL5yWn59fYSwiIkJr167VzJkztXDhQi1ZskTx8fEaPXq00tPTlZCQ4LH67Ha77Ha7SktLPbZOAACA2rB6fpLIUAAAwHqsnqHITwAABBebYRiGr4vwd4WFhYqNjZXD4VBMTIyvywEAACZwHvcd3nsAAPwT53Df4b0HAMB/mTmPB9Qz7wAAAAAAAAAAAAB/RvMOAAAAAAAAAAAAsAiad7Vgt9uVnJyslJQUX5cCAADgN8hQAAAA5pCfAAAILjzzzgO43zgAAP6L87jv8N4DAOCfOIf7Du89AAD+i2feAQAAAAAAAAAAAH6I5h0AAAAAAAAAAABgETTvaoH7jQMAAJhHhgIAADCH/AQAQHDhmXcewP3GAQDwX5zHfYf3HgAA/8Q53Hd47wEA8F888w4AAAAAAAAAAADwQzTvAAAAAAAAAAAAAIugeQcAAAAAAAAAAABYBM07AAAAAAAAAAAAwCJo3tWC3W5XcnKyUlJSfF0KAACA3yBDAQAAmEN+AgAguNgMwzB8XYS/KywsVGxsrBwOh2JiYnxdDgAAMIHzuO/w3gMA4J84h/sO7z0AAP7LzHmcb94BAAAAAAAAAAAAFkHzDgAAAAAAAAAAALAImncAAAAAAAAAAACARdC8AwAAAAAAAAAAACyC5h0AAAAAAAAAAABgETTvasFutys5OVkpKSm+LgUAAMBvkKEAAADMIT8BABBcbIZhGL4uwt8VFhYqNjZWDodDMTExvi4HAACYwHncd3jvAQDwT5zDfYf3HgAA/2XmPM437wAAAAAAAAAAAACLoHkHAAAAAAAAAAAAWATNOwAAAAAAAAAAAMAiaN4BAAAAAAAAAAAAFkHzDgAAAAAAAAAAALAImncAAAAAAAAAAACARdC8AwAAAAAAAAAAACyC5h0AAAAAAAAAAABgEaG+LsCf2e122e12lZaWenU7SU+s8Or6A0nurCG+LgEATOEY7zqO8YGjrjIUAABAoCA/AQAQXPjmXS2kpaUpJydHWVlZvi4FAADAb5ChAAAAzCE/AQAQXGjeAQAAAAAAAAAAABZB8w4AAAAAAAAAAACwCJp3AAAAAAAAAAAAgEXQvAMAAAAAAAAAAAAsguYdAAAAAAAAAAAAYBFB37x78803ZbPZKv2sW7fO16UBAABYEvkJAADAPDIUAABwVaivC7CKjRs3ql69es7XycnJPqwGAADA+shPAAAA5pGhAADAxdC8+/+uuuoqhYbydgAAALiK/AQAAGAeGQoAAFxM0N82EwAAAAAAAAAAALAKSzfvtm3bplmzZmn48OFKSEhw3gv8YoqKijR16lR17txZERERatWqlcaMGaODBw9Wu0zr1q0VGhqqbt26afHixZ7cDQAAgDpDfgIAADCPDAUAAKzE0t/RT09P19KlS00tc+bMGV1//fXKyMhQy5YtNWzYMOXm5mrevHn6+OOPlZGRofbt2zvnb9mypZ555hldddVVKioq0htvvKE77rhDS5Ys0bBhwzy9SwAAAF5FfgIAADCPDAUAAKzEZhiG4esiqvPcc8/p1KlTSklJUUpKipKSklRcXKyaSp48ebKeeeYZpaamatWqVYqKipIkzZ49WxMnTlT//v21bt26Grfbr18/lZSUKDMz06U6CwsLFRsbK4fDoZiYGJf3z1VJT6zw+DoDVe6sIb4uAQBM4RjvOm8d4719Hq9r/pKfpMB77wEACBaBeA73lwwViO89AADBwsx53NLNu5+LiIioMTiVlJSoWbNmcjgc2r59u3r27FlhvHv37srOztbWrVvVu3fvarfz4osv6sknn1RxcbFLddG8sw6adwD8Dcd419G8c49V85MU+O89AACBKhjO4VbNUMHw3gMAEKjMnMct/cw7szZt2iSHw6EOHTpUCk2SNGLECEnS8uXL67o0AAAASyI/AQAAmEeGAgAA3hRQzbudO3dKknr16lXlePn07OzsatdhGIY++uijKoMXAABAoCE/AQAAmEeGAgAA3hTq6wI8ad++fZKkhISEKsfLp+fl5TmnjRgxQldeeaW6deum4uJivf7669qyZYuWLVtW7XaKi4sr3M6gsLDQE+UDAADUubrKTxIZCgAABA4+gwIAAN4UUM27kydPSpIiIyOrHG/YsKEk6cSJE85pnTt31uuvv64DBw5Iknr27KmPP/5YgwcPrnY7M2fO1PTp0z1VNgAAgM/UVX6SyFAAACBw8BkUAADwpoC6baY7nn32We3evVunT5/W6dOntWnTpot+8DRp0iQ5HA7nz/79++uoWgAAAN9zJz9JZCgAABDc+AwKAAC4KqC+eRcVFSVJOn36dJXjp06dkiRFR0fXajvh4eEKDw+X3W6X3W5XaWlprdYHAADgK3WVnyQyFAAACBx8BgUAALwpoL55l5iYKEnO2w/8XPn0tm3bemR7aWlpysnJUVZWlkfWBwAAUNfqOj9JZCgAAOD/+AwKAAB4U0A177p37y5J2r59e5Xj5dO7detWZzUBAABYGfkJAADAPDIUAADwpoBq3vXt21exsbH6/vvvtWPHjkrjixcvliQNHTrUI9uz2+1KTk5WSkqKR9YHAABQ1+o6P0lkKAAA4P/4DAoAAHhTQDXvwsLCNH78eElltxMov7+4JM2ePVvZ2dnq37+/evfu7ZHtccsCAADg7+o6P5VvhwwFAAD8GZ9BAQAAbwr1dQE1WbFihdLT052vS0pKJEl9+vRxTpsyZYqGDBnifD158mStXr1amzdvVqdOndSvXz/l5eUpMzNTTZs21dy5c+tuBwAAAOoY+QkAAMA8MhQAALASSzfv8vPzlZmZWWn6hdPy8/MrjEVERGjt2rWaOXOmFi5cqCVLlig+Pl6jR49Wenq6EhISPFaf3W6X3W5XaWmpx9YJAABQG1bPTxIZCgAAWI/VMxT5CQCA4GIzDMPwdRH+rrCwULGxsXI4HIqJifH4+pOeWOHxdQaq3FlDLj4TAFgIx3jXeesY7+3zOKrHew8AgH/iHO47vPcAAPgvM+fxgHrmHQAAAAAAAAAAAODPaN4BAAAAAAAAAAAAFkHzrhbsdruSk5OVkpLi61IAAAD8BhkKAADAHPITAADBheZdLaSlpSknJ0dZWVm+LgUAAMBvkKEAAADMIT8BABBcQn1dAAAAAAAAAAALsNl8XYH/MAxfVwAACGB88w4AAAAAAAAAAACwCJp3AAAAAAAAAAAAgEXQvKsFHhYMAABgHhkKAADAHPITAADBheZdLfCwYAAAAPPIUAAAAOaQnwAACC407wAAAAAAAAAAAACLoHkHAAAAAAAAAAAAWATNOwAAAAAAAAAAAMAiaN7VAg8LBgAAMI8MBQAAYA75CQCA4ELzrhZ4WDAAAIB5ZCgAAABzyE8AAAQXmncAAAAAAAAAAACARdC8AwAAAAAAAAAAACyC5h0AAAAAAAAAAABgETTvAAAAAAAAAAAAAIugeQcAAAAAAAAAAABYBM07AAAAAAAAAAAAwCJo3tWC3W5XcnKyUlJSfF0KAACA3yBDAQAAmEN+AgAguNC8q4W0tDTl5OQoKyvL16UAAAD4DTIUAACAOeQnAACCC807AAAAAAAAAAAAwCJo3gEAAAAAAAAAAAAWQfMOAAAAAAAAAAAAsAiadwAAAAAAAAAAAIBFhPq6AAAAAMAbbDabr0vwG4Zh+LoEAAAAAADw//HNOwAAAAAAAAAAAMAiaN4BAAAAAAAAAAAAFkHzDgAAAAAAAAAAALAImne1YLfblZycrJSUFF+XAgAA4DfIUAAAAOaQnwAACC4072ohLS1NOTk5ysrK8nUpAAAAfoMMBQAAYA75CQCA4ELzDgAAAAAAAAAAALAImncAAAAAAAAAAACARdC8AwAAAAAAAAAAACyC5h0AAAAAAAAAAABgETTvLvDVV18pNDRUCQkJvi4FAADAL5CfAAAAzCNDAQCAmtC8u8DDDz+sxo0b+7oMAAAAv0F+AgAAMI8MBQAAakLz7v9bsmSJfvjhB40ZM8bXpQAAAPgF8hMAAIB5ZCgAAHAxNO8klZSU6H//9381a9YshYeH+7ocAAAAyyM/AQAAmEeGAgAArrB0827btm2aNWuWhg8froSEBNlsNtlstosuV1RUpKlTp6pz586KiIhQq1atNGbMGB08eLDK+V955RU1bdpUd955p6d3AQAAoE6RnwAAAMwjQwEAACsJ9XUBNUlPT9fSpUtNLXPmzBldf/31ysjIUMuWLTVs2DDl5uZq3rx5+vjjj5WRkaH27ds75z9y5IieeeYZrVy50tPlAwAA1DnyEwAAgHlkKAAAYCWW/uZdamqqpkyZomXLlunQoUMu3U5gxowZysjIUGpqqnbv3q1FixYpMzNTL730kvLz8yvdT/yPf/yjBg0apNTUVG/tBgAAQJ0hPwEAAJhHhgIAAFZi6W/ePf7446bmLykp0Zw5cyRJdrtdUVFRzrEJEyZo/vz5Wr9+vbZt26bevXvr66+/1ttvv62MjAwVFBRIKrtqyjAMFRQUKDIyUmFhYR7bHwAAAG8jPwEAAJhHhgIAAFZi6W/embVp0yY5HA516NBBPXv2rDQ+YsQISdLy5cslSXv27FFJSYl69eqlRo0aqVGjRnruuef0448/qlGjRpo7d26d1g8AAFDXyE8AAADmkaEAAIA3Wfqbd2bt3LlTktSrV68qx8unZ2dnS5KuueYarV27tsI8b775plasWKEPPvhAnTt39mK1AAAAvkd+AgAAMI8MBQAAvCmgmnf79u2TJCUkJFQ5Xj49Ly9PktSkSRMNGDCgwjzr1q1TeHh4pekXKi4uVnFxsfN1YWFhLaoGAADwnbrKTxIZCgAABA4+gwIAAN4UULfNPHnypCQpMjKyyvGGDRtKkk6cOFGr7cycOVOxsbHOnzZt2tRqfQAAAL5SV/lJIkMBAIDAwWdQAADAmzzevPv222+1ePFiZWZmenrVdWLatGk6cOBAjfNMmjRJDofD+bN///46qg4AAAQqf85QruQniQwFAAA8y5/zk8RnUAAAoHpuNe8WLVqk66+/vlI4evTRR3X55Zfrzjvv1NVXX63bbrtNpaWlHinUFVFRUZKk06dPVzl+6tQpSVJ0dHStthMeHq6YmJgKPwAAABdjxQxVV/lJIkMBAADzrJifJD6DAgAA3uVW8+7tt9/Wjh071LNnT+e0zZs366WXXlJ0dLTuuusuJSUladmyZXrnnXc8VuzFJCYmSlK1Vy2VT2/btq1Htme325WcnKyUlBSPrA8AAAQ2K2aous5PEhkKAAC4zor5SeIzKAAA4F1uNe++/vprdevWTWFhYc5pCxYskM1m0/vvv6933nlHWVlZioqK0uuvv+6xYi+me/fukqTt27dXOV4+vVu3bh7ZXlpamnJycpSVleWR9QEAgMBmxQxV1/lJIkMBAADXWTE/SXwGBQAAvMut5t1//vMftW7dusK0tWvXqlmzZvrFL34hSYqPj9e1116rPXv21L5KF/Xt21exsbH6/vvvtWPHjkrjixcvliQNHTq0zmoCAAAoZ8UMRX4CAABWZsX8JJGhAACAd7nVvGvQoIEKCwudrw8dOqTdu3erf//+FeaLi4vT8ePHa1ehCWFhYRo/fryksiuSyu8vLkmzZ89Wdna2+vfvr969e3tke9yyAAAAmGHFDFXX+UkiQwEAANdZMT9JfAYFAAC8K9Sdhdq3b68NGzaooKBAcXFxeuedd2Sz2ZxXPJU7fPiwmjVr5nZxK1asUHp6uvN1SUmJJKlPnz7OaVOmTNGQIUOcrydPnqzVq1dr8+bN6tSpk/r166e8vDxlZmaqadOmmjt3rtv1/FxaWprS0tJUWFio2NhYj60XAAAEprrIUFbPTxIZCgAAuI7PoMqQnwAACC5uNe9Gjx6t8ePHq3fv3urRo4dWrFihqKgoDRs2zDnP2bNntXXrVl1xxRVuF5efn6/MzMxK0y+clp+fX2EsIiJCa9eu1cyZM7Vw4UItWbJE8fHxGj16tNLT05WQkOB2PQAAALVRFxmK/AQAAAIJn0EBAIBgZDMMwzC70NmzZ3XPPffoww8/lCQ1bNhQf//733XXXXc55/noo490++2365lnntGkSZM8V7GF2O122e12lZaWavfu3XI4HIqJifH4dpKeWOHxdQaq3FlDLj4TAFgIx3jXeesYX371srfO4xciQ5Wpqwxls9k8vs5A5cY/CQAAQYz8VPfqKj+J/OQ68hMAwCQzGcqt5l253Nxc5efn69JLL1V0dHSFsR07digvL099+vRR8+bN3d2EX/B2aOWDXdfRvAPgbzjGuy4QmnflyFBlvP3e07xzHc07AIAZ5Cff8fp7T35yHfkJAGCSmfO4W7fNLJeUlKSkpKQqx3r06KEePXrUZvUAAAABiQwFAABgDvkJAAAEkxB3FqpXr54eeOCBi873m9/8RqGhteoPAgAABAwyFAAAgDnkJwAAEIzcat4ZhuHyrXUC+RY8drtdycnJSklJ8XUpAADAD5ChypChAACAq8hPZchPAAAEF7ead65yOBwKDw/35iZ8Ki0tTTk5OcrKyvJ1KQAAIICQoQAAAMwhPwEAgEDi8v0E9u3bV+H1yZMnK00rd+7cOX333XdatWqVOnToULsKAQAA/BgZCgAAwBzyEwAACHYuN++SkpJks9mcrz/88EN9+OGHNS5jGIZ+85vfuF8dAACAnyNDAQAAmEN+AgAAwc7l5t21117rDE7r169Xs2bNdOmll1Y5b1hYmFq1aqVbbrlFt912m2cqtSC73S673a7S0lJflwIAACyKDFUZGQoAANSE/FQZ+QkAgOBiM9x4mm9ISIhGjx6tuXPneqMmv1NYWKjY2Fg5HA7FxMR4fP1JT6zw+DoDVe6sIb4uAQBM4RjvOm8d4719Hr8QGaoib7/3F16xj5q58U8CAEAQIz/5jtffe/KT68hPAACTzJzHXf7m3YX27t2rqKgot4oDAAAIVmQoAAAAc8hPAAAgGLnVvGvbtq2n6wAAAAh4ZCgAAABzyE8AACAYudW8K7du3Tp9/vnnOnTokIqLi6ucx2az6Y033qjNZgAAAAIKGQoAAMAc8hMAAAgmbjXvHA6Hhg0bpg0bNlz0+RgEJwAAgDJkKAAAAHPITwAAIBi51bx7/PHH9fnnn6tjx44aN26cOnfurOjoaE/XZnl2u112u12lpaW+LgUAAPgBMlQZMhQAAHAV+akM+QkAgOBiMy522VIVWrZsKUn65ptvFB8f7/Gi/E1hYaFiY2PlcDgUExPj8fUnPbHC4+sMVLmzhvi6BAAwhWO867x1jPf2efxCZKiKvP3e22w2j68zULnxTwIAQBAjP/mO19978pPryE8AAJPMnMdD3NmAw+FQ3759CU0AAAAmkKEAAADMIT8BAIBg5FbzrlOnTjp16pSnawEAAAhoZCgAAABzyE8AACAYudW8+/3vf69169Zpz549nq4HAAAgYJGhAAAAzCE/AQCAYORW8+7Xv/61/vCHP6h///6aN2+eDhw44Om6AAAAAg4ZCgAAwBzyEwAACEah7ixUr149SWUPtv/1r39d47w2m03nzp1zZzMAAAABhQwFAABgDvkJAAAEI7ead23atJHNZvN0LX7HbrfLbrertLTU16UAAAA/QIYqQ4YCAACuIj+VIT8BABBcbIZhGL4uwt8VFhYqNjZWDodDMTExHl9/0hMrPL7OQJU7a4ivSwAAUzjGu85bx3hvn8dRPW+/93zQ5zr+SQAAMIP85Dtef+/JT64jPwEATDJzHnfrmXcAAAAAAAAAAAAAPI/mHQAAAAAAAAAAAGARbj3z7q233jI1/3333efOZgAAAAIKGQoAAMAc8hMAAAhGbjXvRo8e7dIzRAzDkM1mIzgBAACIDAUAAGAW+QkAAAQjt5p3U6dOrTI4nT9/Xvv379f69eu1d+9ejR49Wm3btq11kQAAAIGADAUAAGAO+QkAAAQjt5p306ZNq3H83LlzmjBhghYtWqSsrCx3NgEAABBwyFAAAADmkJ8AAEAwCvHGSkNDQ/Xyyy+rQYMGeuKJJ7yxCQAAgIBDhgIAADCH/AQAAAKRV5p3klSvXj317t1bn332mbc2AQAAEHDIUAAAAOaQnwAAQKDxWvNOkg4fPqxTp055cxM+ZbfblZycrJSUFF+XAgAAAggZCgAAwBzyEwAACCRead6dP39ef/7zn7VlyxZ169bNG5uwhLS0NOXk5HBPdQAA4BFkKAAAAHPITwAAIBCFurPQ9ddfX+3YyZMntXfvXh07dkwhISF66qmn3C4OAAAgkJChAAAAzCE/AQCAYORW827dunU1rzQ0VNdcc42mTp2qgQMHurMJAACAgEOGAgAAMIf8BAAAgpFbzbu9e/dWOxYWFqYmTZqofv36bhcFAAAQiMhQAAAA5pCfAABAMHKrede2bVtP1wEAABDwyFAAAADmkJ8AAEAwCvF1AQAAAAAAAAAAAADK1Kp5l52drbFjxyo5OVmxsbGKjY1VcnKyxo0bp+zsbE/V6FXz58/XFVdcobi4ODVs2FC9evXSe++95+uyAABAAPP3DEV+AgAAdc3f85NEhgIAAK5z67aZkvSnP/1Jjz76qEpLS2UYhnP6rl27tGvXLs2dO1cvvPCCHnroIY8U6i3Hjx/Xrbfeqh49eigiIkJLlizR3XffrYiICN16662+Lg8AAASYQMhQ5CcAAFCXAiE/SWQoAADgOptxYepx0WeffaabbrpJkZGRGjdunEaNGqWkpCTZbDbl5uZqwYIFeu2113T69GmtWrVKAwcO9EbtXnPNNdeoZcuW+uCDD1yav7CwULGxsXI4HIqJifF4PUlPrPD4OgNV7qwhvi4BAEzhGO86bx3jvX0ev1AgZyiz+Uny/ntvs9k8vs5A5cY/CQAAQYz85DlW+wxK5CfXkZ8AACaZOY+7ddvM2bNnKzQ0VKtWrdKLL76o7t27KzY2VjExMerWrZteeOEFrVq1SiEhIXrppZfc2glfaty4sc6ePevrMgAAQIAJ5AxFfgIAAN4QyPlJIkMBAICqudW8++KLL9S/f39dffXV1c6TmpqqAQMGKDMz0+3itm3bplmzZmn48OFKSEiQzWZz6QrqoqIiTZ06VZ07d1ZERIRatWqlMWPG6ODBg9Uuc+7cORUWFmrRokX67LPPNHbsWLfrBgAAqEpdZCjyEwAACCR8BgUAAIKRW8+8O336tJo2bXrR+Zo2barTp0+7swlJUnp6upYuXWpqmTNnzuj6669XRkaGWrZsqWHDhik3N1fz5s3Txx9/rIyMDLVv377CMocPH1bLli0lSfXq1dNf/vIX3XzzzW7XDQAAUJW6yFDkJwAAEEj4DAoAAAQjt5p3bdq00ZYtW3Tu3DmFhla9inPnzmnLli1q06aN28WlpqaqW7duSklJUUpKipKSklRcXFzjMjNmzFBGRoZSU1O1atUqRUVFSSq7zcLEiRM1ZswYrVu3rsIyTZo0UVZWlk6cOKGVK1dq/Pjxaty4sW6//Xa3awcAAPi5ushQ5CcAABBI+AwKAAAEI5vhxtPpH330Ub300kv65S9/qVdffVVxcXEVxgsLC/XQQw/prbfe0sSJE/X88897pNiIiAgVFxerupJLSkrUrFkzORwObd++XT179qww3r17d2VnZ2vr1q3q3bt3tdv5zW9+o/Xr12v37t0u1eXthwUnPbHC4+sMVLmzhvi6BAAwhWO867x1jPf2efxCvshQVs1Pkvffe1dudYUybvyTAAAQxAI9P0nWzVBef+/JT64jPwEATDJzHnfrm3eTJk3SP/7xD73zzjtaunSpBg0apKSkJElSXl6eVq5cqcLCQrVv316TJk1yZxNu2bRpkxwOhzp06FApNEnSiBEjlJ2dreXLl9cYnHr06KF58+Z5s1QAABCErJihyE8AAMDKrJifJDIUAADwLread/Hx8dqwYYPGjh2rFStW6IMPPqg0z5AhQ/TXv/5VjRo1qnWRrtq5c6ckqVevXlWOl0/Pzs6ucT2bN292BsGqFBcXV7h1QmFhoclKAQBAMLJihqqr/CSRoQAAgHlWzE8Sn0EBAADvcqt5J0mtWrXS8uXLtXfvXm3cuFE//vijc/o111yjdu3aeaxIV+3bt0+SlJCQUOV4+fS8vDzntOuuu0633367Lr30Up05c0ZLly7VwoUL9be//a3a7cycOVPTp0/3YOUAACBYWC1D1VV+kshQAADAPVbLTxKfQQEAAO9yu3lXrl27dj4JSVU5efKkJCkyMrLK8YYNG0qSTpw44ZzWvXt3/fnPf9b+/fvVsGFDJScna/ny5fqf//mfarczadIkTZgwwfm6sLCwVg9FBgAAwccqGaqu8pNEhgIAALVjlfwk8RkUAADwLpebd99++63y8/OVlJSkxMTEGufNy8tTXl6emjVrpksvvbTWRXrTK6+8oldeecXUMuHh4QoPD/dOQQAAIKAEYoZyJz9JZCgAAOCaQMxPEp9BAQAA17nUvDt69KhSU1MVGRmpbdu2XXT+sLAw3XXXXSopKdGePXsUFxdX2zpdEhUVJUk6ffp0leOnTp2SJEVHR3tke3a7XXa7XaWlpR5ZHwAACCz+kKHqOj9JZCgAAFA9f8hPEp9BAQAA7wpxZaZ58+apsLBQzz77rFq2bHnR+Vu2bKlZs2bp2LFjmjdvXq2LdFX51VgHDhyocrx8etu2bT2yvbS0NOXk5CgrK8sj6wMAAIHFHzJUXecniQwFAACq5w/5SeIzKAAA4F0uNe9WrFih2NhY/fKXv3R5xffee6/i4uK0bNkyt4szq3v37pKk7du3VzlePr1bt251VhMAAAhe/pChyE8AAMBK/CE/SWQoAADgXS4177755hv16dNHoaEuPyJP9erV01VXXaVvvvnG7eLM6tu3r2JjY/X9999rx44dlcYXL14sSRo6dKhHtme325WcnKyUlBSPrA8AAAQWf8hQdZ2fJDIUAAConj/kJ4nPoAAAgHe51LxzOBxq3Lix6ZU3btxYDofD9HLuCgsL0/jx4yWV3U6g/P7ikjR79mxlZ2erf//+6t27t0e2xy0LAABATfwhQ9V1firfDhkKAABUxR/yk8RnUAAAwLtcuowpLi5Ox44dM73yY8eOKSYmxvRy5VasWKH09HTn65KSEklSnz59nNOmTJmiIUOGOF9PnjxZq1ev1ubNm9WpUyf169dPeXl5yszMVNOmTTV37ly36wEAADDDFxmK/AQAAPwZn0EBAAC42Lzr2LGjMjMzVVpaqnr16rm04nPnzikjI0OXXnqp28Xl5+crMzOz0vQLp+Xn51cYi4iI0Nq1azVz5kwtXLhQS5YsUXx8vEaPHq309HQlJCS4Xc/P2e122e12lZaWemydAAAgcPgiQ1k9P0lkKAAAUD0+g6oa+QkAgODi0m0zBw0apIKCAs2ZM8flFc+ZM0cOh0M333yz28WNHj1ahmHU+DN69OhKyzVo0EBPP/209uzZo+LiYh06dEjz5s3z+AdP3LIAAADUxBcZyur5SSJDAQCA6vEZVNXITwAABBeXmnfjx49XVFSUHnvsMb399tsXnX/BggV67LHHFB0drbS0tFoXCQAA4I/IUAAAAOaQnwAAAFy8bWZ8fLzmz5+vESNG6P7779f//d//6e6771avXr3UtGlTSWW3Dti+fbveffddZWRkyGazaf78+YqPj/fqDgAAAFgVGQoAAMAc8hMAAICLzTtJuvXWW7V8+XLdf//92rJlizIyMqqczzAMNW3aVG+++WatblfgD7jfOAAAuBgyVGVkKAAAUBPyU2XkJwAAgovNMAzDzAKnT5/W/Pnz9cknn2jHjh366aefJEmNGzdWjx49NGTIEN13332KjIz0SsFWVFhYqNjYWDkcDsXExHh8/UlPrPD4OgNV7qwhvi4BAEzhGO86bx3jvX0eL0eGqszb773NZvP4OgOVyX8SAACCHPnJd7z+3pOfXEd+AgCYZOY8brp5h8po3lkHzTsA/oZjvOv8vXmHymjeWQf/JAAAmEF+8h2adxZCfgIAmGTmPB5SRzUBAAAAAAAAAAAAuAiadwAAAAAAAAAAAIBF0LyrBbvdruTkZKWkpPi6FAAAAL9BhgIAADCH/AQAQHCheVcLaWlpysnJUVZWlq9LAQAA8BtkKAAAAHPITwAABBeadwAAAAAAAAAAAIBF0LwDAAAAAAAAAAAALILmHQAAAAAAAAAAAGARNO9qgYcFAwAAmEeGAgAAMIf8BABAcLEZhmGYXeitt95yab6wsDA1btxY3bt3V7NmzUwX5y8KCwsVGxsrh8OhmJgYj68/6YkVHl9noMqdNcTXJQCAKRzjXeetY7y3z+MXIkNV5O333mazeXydgcqNfxIAAIIY+cl3vP7ek59cR34CAJhk5jwe6s4GRo8eberDEJvNphtuuEF//vOf1alTJ3c2CQAA4PfIUAAAAOaQnwAAQDByq3k3depU5ebm6q233lJUVJR+8YtfKDExUZK0f/9+rVq1SidOnNCoUaMUHh6uzZs3a9WqVerXr5+2bdum1q1be3QnAAAA/AEZCgAAwBzyEwAACEZuNe9GjRqlK6+8UmPGjNFLL72k2NjYCuOFhYWaMGGCPvroI2VmZqp9+/Z69NFH9fLLL2vWrFn685//7JHiAQAA/AkZCgAAwBzyEwAACEZuPfNu5MiR2r59u3bv3q2QkJAq5zl//rw6d+6sXr166f3331dJSYnatWunyMhI/fvf/6514VbCM++sg2feAfA3HONdFwjPvCNDVcQz76yDZ94BAMwgP/kOz7yzEPITAMAkM+fxqlPPRaxdu1ZXXXVVtaFJkkJCQnTllVdqzZo1ksoeHNy9e3cdPHjQnU0CAAD4PTIUAACAOeQnAAAQjNxq3p0+fVqHDx++6HxHjhzRmTNnnK9jYmIUGurWnTotyW63Kzk5WSkpKb4uBQAA+AEyVBkyFAAAcBX5qQz5CQCA4OJW865r1676/PPP9fnnn1c7z4YNG7R+/Xp17drVOW3//v1q2rSpO5u0pLS0NOXk5CgrK8vXpQAAAD9AhipDhgIAAK4iP5UhPwEAEFzcat499thjKi0t1U033aSxY8fqs88+065du7Rr1y599tlnGjdunG666SYZhqHHHntMkuRwOLRt2zb16dPHozsAAADgL8hQAAAA5pCfAABAMLIZbj6d/k9/+pMef/xxlZSUyPazh9kahqGwsDA9//zz+sMf/iBJ+uGHH/TBBx9o4MCBuuKKK2pfuYV4+2HBSU+s8Pg6A1XurCG+LgEATOEY7zpvHeO9fR7/OTLUf3n7vf/5+4vquflPAgBAkCI/+Y7X33vyk+vITwAAk8ycx91u3knS3r179cYbb2jz5s06dOiQJKlly5bq27evfvWrX6l9+/burtqv0LyzDpp3APwNx3jXBUrzTiJDlaN5Zx007wAAZpCffIfmnYWQnwAAJpk5j9fqyb3t2rXTjBkzarMKAACAoEOGAgAAMIf8BAAAgolbz7wDAAAAAAAAAAAA4Hm1+ubdkSNHNHfuXG3YsEEHDx6UJLVu3VrXXnutfvWrX6l58+YeKRIAACCQkKEAAADMIT8BAIBg4vYz7z788EONGTNGJ0+erPSMDJvNpujoaL3xxhu6/fbbPVKolfHMO+vgmXcA/A3HeNcFyjPvyFD/xTPvrINn3gEAzCA/+Q7PvLMQ8hMAwCQz53G3bpu5detW3X333Tp16pRuu+02ffTRR/ryyy+1Y8cOLVmyRMOHD9fJkyd1zz33aOvWrW7tBAAAQKAhQwEAAJhDfgIAAMHIrdtmzpw5U6WlpVq8eLFuu+22CmPdunXTLbfcoo8++ki33367Zs2apcWLF3ukWAAAAH9GhgIAADCH/AQAAIKRW9+827hxo66++upKoelCt912m/r27asNGza4XZzV2e12JScnKyUlxdelAAAAP0CGKkOGAgAAriI/lSE/AQAQXNxq3jkcDiUmJl50vsTERDkcDnc24RfS0tKUk5OjrKwsX5cCAAD8ABmqDBkKAAC4ivxUhvwEAEBwcat516JFC3355ZcXnW/Hjh1q0aKFO5sAAAAIOGQoAAAAc8hPAAAgGLnVvLvpppv03Xff6Y9//KNKS0srjRuGocmTJ2vXrl0aNGhQrYsEAAAIBGQoAAAAc8hPAAAgGNkMwzDMLnTgwAH17NlTx44dU2JiokaOHKmkpCRJUl5enj744APl5uaqcePG2r59uxISEjxdt6UUFhYqNjZWDodDMTExHl9/0hMrPL7OQJU7a4ivSwAAUzjGu85bx3hvn8cvRIaqyNvvvc1m8/g6A5Ub/yQAAAQx8pPveP29Jz+5jvwEADDJzHk81J0NJCQkaM2aNbr33nv19ddf64UXXnB+OFL+D/+uXbvqnXfeCfjQBAAA4CoyFAAAgDnkJwAAEIzcat5JZcEoOztb69at04YNG/Tjjz9Kklq1aqV+/fppwIABnqrRq95//33Nnz9f27dv1+nTp9W9e3c9++yzuuaaa3xdGgAACEBkKAAAAHPITwAAINi43bwrN2DAgGpD0ty5c3XgwAFNnTq1tpvxmldeeUWdOnWS3W5XVFSU5s2bp4EDB+qLL75Q9+7dfV0eAAAIUGQoAAAAc8hPAAAgWLj1zDtXpaam6osvvqjygcJW8dNPP6lx48bO1+fPn1fXrl3Vt29f/e1vf3NpHTzzzjp45h0Af8Mx3nWB8Mw7V5GhPINn3rmOZ94BAMwgP7nHH/ITz7wzgfwEADDJzHk8pI5qsqwLQ5MkhYSE6PLLL9fevXt9VBEAAID1kaEAAADMIT8BAABXWbp5t23bNs2aNUvDhw9XQkKCbDabS1dQFxUVaerUqercubMiIiLUqlUrjRkzRgcPHrzosqWlpcrKylLHjh09sQsAAAB1jgwFAABgDvkJAABYSa2feedN6enpWrp0qallzpw5o+uvv14ZGRlq2bKlhg0bptzcXM2bN08ff/yxMjIy1L59+2qXnzNnjvbt26ff/e53tS0fAADAJ8hQAAAA5pCfAACAlVj6m3epqamaMmWKli1bpkOHDik8PPyiy8yYMUMZGRlKTU3V7t27tWjRImVmZuqll15Sfn6+xowZU+2ymZmZeuKJJzR58mR17drVk7sCAABQZ8hQAAAA5pCfAACAldgMLz6d3tMPC46IiFBxcbGqK7mkpETNmjWTw+HQ9u3b1bNnzwrj3bt3V3Z2trZu3arevXtXGMvNzVWfPn107bXXatGiRS7dGqGctx8WnPTECo+vM1Dlzhri6xIAwBSO8a7z1jHe2+dxd5ChPMNMLcHOi/8kAAAEIPJT4OYnkZ9cR34CAJhk5jxu6W/embVp0yY5HA516NChUmiSpBEjRkiSli9fXmF6QUGBhgwZoqSkJM2fP58PegAAQFAhQwEAAJhDfgIAAN7k0jPv6tWr5+06PGLnzp2SpF69elU5Xj49OzvbOa2kpETDhw/X6dOntWbNGjVo0MD7hQIAgKBAhgIAADCH/AQAAOBi8642t9GpyyuI9u3bJ0lKSEiocrx8el5ennPa7373O61fv15///vftXfvXu3du1eSFB4eXuWVU5JUXFys4uJi5+vCwkKP1A8AAAILGaoiMhQAALgY8lNF5CcAAIKTS8278+fPe7sOjzh58qQkKTIyssrxhg0bSpJOnDjhnLZ69WqdP39eDzzwQIV527Ztq9zc3CrXM3PmTE2fPt0DFQMAgEBGhqqIDAUAAC6G/FQR+QkA4DHcqtl1FniuaUA9884dubm5Mgyj0k91oUmSJk2aJIfD4fzZv39/3RUMAABgAWQoAAAAc8hPAADAVS59885fREVFSZJOnz5d5fipU6ckSdHR0bXaTnh4uMLDw2W322W321VaWlqr9QEAAPgSGQoAAMAc8hMAAPCmgPrmXWJioiTpwIEDVY6XT2/btq1HtpeWlqacnBxlZWV5ZH0AAAC+QIYCAAAwh/wEAAC8KaCad927d5ckbd++vcrx8undunWrs5oAAACsjgwFAABgDvkJAAB4U0A17/r27avY2Fh9//332rFjR6XxxYsXS5KGDh3qke3Z7XYlJycrJSXFI+sDAADwBTIUAACAOeQnAADgTQHVvAsLC9P48eMlld1OoPz+4pI0e/ZsZWdnq3///urdu7dHtsctCwAAQCAgQwEAAJhDfgIAAN4U6usCarJixQqlp6c7X5eUlEiS+vTp45w2ZcoUDRkyxPl68uTJWr16tTZv3qxOnTqpX79+ysvLU2Zmppo2baq5c+fW3Q4AAAD4ABkKAADAHPITAACwEks37/Lz85WZmVlp+oXT8vPzK4xFRERo7dq1mjlzphYuXKglS5YoPj5eo0ePVnp6uhISErxeNwAAgC+RoQAAAMwhPwEAACuxGYZh+LoIf2W322W321VaWqrdu3fL4XAoJibG49tJemKFx9cZqHJnDbn4TABgIRzjXeetY3xhYaFiY2O9dh5HZXWVoWw2m8fXGaj4JwEAwAzyU92rq/wk8pPryE8A/A3HeNd56RhvJkMF1DPv6hr3GwcAADCPDAUAAGAO+QkAgOBC8w4AAAAAAAAAAACwCJp3AAAAAAAAAAAAgEXQvKsFu92u5ORkpaSk+LoUAAAAv0GGAgAAMIf8BABAcKF5VwvcbxwAAMA8MhQAAIA55CcAAIILzTsAAAAAAAAAAADAImjeAQAAAAAAAAAAABZB864WuN84AACAeWQoAAAAc8hPAAAEF5p3tcD9xgEAAMwjQwEAAJhDfgIAILjQvAMAAAAAAAAAAAAsguYdAAAAAAAAAAAAYBE07wAAAAAAAAAAAACLoHkHAAAAAAAAAAAAWESorwvwZ3a7XXa7XaWlpb4uBQAAwG+QoQAAtWGz2Xxdgl8xDMPXJcADyE8AAAQXvnlXC2lpacrJyVFWVpavSwEAAPAbZCgAAABzyE8AAAQXmncAAAAAAAAAAACARdC8AwAAAAAAAAAAACyC5h0AAAAAAAAAAABgETTvAAAAAAAAAAAAAIugeQcAAAAAAAAAAABYBM27WrDb7UpOTlZKSoqvSwEAAPAbZCgAAABzyE8AAAQXmne1kJaWppycHGVlZfm6FAAAAL9BhgIAADCH/AQAQHCheQcAAAAAAAAAAABYBM07AAAAAAAAAAAAwCJo3gEAAAAAAAAAAAAWQfMOAAAAAAAAAAAAsAiadwAAAAAAAAAAAIBF0LwDAAAAAAAAAAAALILmHQAAAAAAAAAAAGARNO8AAAAAAAAAAAAAi6B5Vwt2u13JyclKSUnxdSkAAAB+gwwFAABgDvkJAIDgQvOuFtLS0pSTk6OsrCxflwIAAOA3yFAAAADmkJ8AAAguNO8AAAAAAAAAAAAAi6B5BwAAAAAAAAAAAFgEzTsAAAAAAAAAAADAImjeAQAAAAAAAAAAABZB8w4AAAAAAAAAAACwiKBv3m3dulX33XefOnbsKJvNpsmTJ/u6JAAAAMsjQwEAAJhDfgIAAK4K+ubdpk2blJGRoWuuuUaxsbG+LgcAAMAvkKEAAADMIT8BAABXhfq6AF/7/e9/r4ceekiSlJSU5NtiAAAA/AQZCgAAwBzyEwAAcFXQf/MuJCTo3wIAAADTyFAAAADmkJ8AAICrLJ0atm3bplmzZmn48OFKSEiQzWaTzWa76HJFRUWaOnWqOnfurIiICLVq1UpjxozRwYMH66BqAAAA3yJDAQAAmEN+AgAAVmLp22amp6dr6dKlppY5c+aMrr/+emVkZKhly5YaNmyYcnNzNW/ePH388cfKyMhQ+/btvVQxAACA75GhAAAAzCE/AQAAK7H0N+9SU1M1ZcoULVu2TIcOHVJ4ePhFl5kxY4YyMjKUmpqq3bt3a9GiRcrMzNRLL72k/Px8jRkzpg4qBwAA8B0yFAAAgDnkJwAAYCWW/ubd448/bmr+kpISzZkzR5Jkt9sVFRXlHJswYYLmz5+v9evXa9u2berdu7dHawUAALAKMhQAAIA55CcAAGAllv7mnVmbNm2Sw+FQhw4d1LNnz0rjI0aMkCQtX768rksDAACwLDIUAACAOeQnAADgTZb+5p1ZO3fulCT16tWryvHy6dnZ2bXaTnFxsYqLi52vCwsLa7U+AAAAXyJDAQAAmEN+AgAA3hRQzbt9+/ZJkhISEqocL5+el5fnnJafn6/169dLkk6fPq1du3Zp8eLFatiwoW6++eYq1zNz5kxNnz7dk6UDAAD4DBkKAADAHPITAADwpoBq3p08eVKSFBkZWeV4w4YNJUknTpxwTvvmm290xx13OF9/+OGH+vDDD9W2bVvl5uZWuZ5JkyZpwoQJzteFhYVq06ZNbcsHAADwCTIUAACAOeQnAADgTQHVvHPHgAEDZBiGqWXCw8MVHh7upYoAAACsjwwFAABgDvkJAAC4KsTXBXhSVFSUpLJbD1Tl1KlTkqTo6GiPbM9utys5OVkpKSkeWR8AAIAvkKEAAADMIT8BAABvCqjmXWJioiTpwIEDVY6XT2/btq1HtpeWlqacnBxlZWV5ZH0AAAC+QIYCAAAwh/wEAAC8KaCad927d5ckbd++vcrx8undunWrs5oAAACsjgwFAABgDvkJAAB4U0A17/r27avY2Fh9//332rFjR6XxxYsXS5KGDh3qke1xywIAABAIyFAAAADmkJ8AAIA3BVTzLiwsTOPHj5dUdjuB8vuLS9Ls2bOVnZ2t/v37q3fv3h7ZHrcsAAAAgYAMBQAAYA75CQAAeFOorwuoyYoVK5Senu58XVJSIknq06ePc9qUKVM0ZMgQ5+vJkydr9erV2rx5szp16qR+/fopLy9PmZmZatq0qebOnVt3OwAAAOADZCgAAABzyE8AAMBKLN28y8/PV2ZmZqXpF07Lz8+vMBYREaG1a9dq5syZWrhwoZYsWaL4+HiNHj1a6enpSkhI8Fh9drtddrtdpaWlHlsnAABAbZGhAAAAzCE/AQAAK7EZhmH4ugh/V1hYqNjYWDkcDsXExHh8/UlPrPD4OgNV7qwhF58JACyEY7zrvHWM9/Z5HNXz9ntvs9k8vs5AxT8JAPgTju/meOMYT37yHa+/9/x+uY78BMDfcIx3nZeO8WbO4wH1zDsAAAAAAAAAAADAn9G8AwAAAAAAAAAAACyC5l0t2O12JScnKyUlxdelAAAA+A0yFAAAgDnkJwAAggvNu1pIS0tTTk6OsrKyfF0KAACA3yBDAQAAmEN+AgAguNC8AwAAAAAAAAAAACyC5h0AAAAAAAAAAABgETTvAAAAAAAAAAAAAIugeVcLPCwYAADAPDIUAACAOeQnAACCC827WuBhwQAAAOaRoQAAAMwhPwEAEFxo3gEAAAAAAAAAAAAWQfMOAAAAAAAAAAAAsAiadwAAAAAAAAAAAIBF0LyrBR4WDAAAYB4ZCgAAwBzyEwAAwYXmXS3wsGAAAADzyFAAAADmkJ8AAAguNO8AAAAAAAAAAAAAi6B5BwAAAAAAAAAAAFgEzTsAAAAAAAAAAADAImjeAQAAAAAAAAAAABZB8w4AAAAAAAAAAACwCJp3tWC325WcnKyUlBRflwIAAOA3yFAAAADmkJ8AAAguNO9qIS0tTTk5OcrKyvJ1KQAAAH6DDAUAAGAO+QkAgOBC8w4AAAAAAAAAAACwCJp3AAAAAAAAAAAAgEXQvAMAAAAAAAAAAAAsguYdAAAAAAAAAAAAYBE07wAAAAAAAAAAAACLoHkHAAAAAAAAAAAAWATNOwAAAAAAAAAAAMAiaN4BAAAAAAAAAAAAFkHzrhbsdruSk5OVkpLi61IAAAD8BhkKAADAHPITAADBheZdLaSlpSknJ0dZWVm+LgUAAMBvkKEAAADMIT8BABBcaN4BAAAAAAAAAAAAFkHzDgAAAAAAAAAAALAImncAAAAAAAAAAACARdC8AwAAAAAAAAAAACyC5h0AAAAAAAAAAABgETTvJO3YsUP9+vVTgwYN1K5dO82ZM8fXJQEAAFga+QkAAMA8MhQAAHBFqK8L8LX8/HzdeOONuvLKK/Xxxx9r+/btevjhhxUbG6tRo0b5ujwAAADLIT8BAACYR4YCAACuCvrm3WuvvSabzaYPPvhAkZGRGjhwoPbu3av09HSCEwAAQBXITwAAAOaRoQAAgKuC/raZ//znPzV48GBFRkY6p91xxx3697//rR9++MGHlQEAAFgT+QkAAMA8MhQAAHCVpZt327Zt06xZszR8+HAlJCTIZrPJZrNddLmioiJNnTpVnTt3VkREhFq1aqUxY8bo4MGDlebdvXu3Lr300grTyl9/9913ntkRAACAOkJ+AgAAMI8MBQAArMTSt81MT0/X0qVLTS1z5swZXX/99crIyFDLli01bNgw5ebmat68efr444+VkZGh9u3bO+c/fvy44uLiKqyjUaNGzjEAAAB/Qn4CAAAwjwwFAACsxNLfvEtNTdWUKVO0bNkyHTp0SOHh4RddZsaMGcrIyFBqaqp2796tRYsWKTMzUy+99JLy8/M1ZsyYOqgcAADAN8hPAAAA5pGhAACAlVj6m3ePP/64qflLSko0Z84cSZLdbldUVJRzbMKECZo/f77Wr1+vbdu2qXfv3pLKrnByOBwV1lNQUOAcAwAA8CfkJwAAAPPIUAAAwEos/c07szZt2iSHw6EOHTqoZ8+elcZHjBghSVq+fLlzWufOnbVr164K85W/vuSSS7xYLQAAgO+RnwAAAMwjQwEAAG8KqObdzp07JUm9evWqcrx8enZ2tnPaTTfdpE8++URFRUXOaYsXL1anTp0q3JccAAAgEJGfAAAAzCNDAQAAb7L0bTPN2rdvnyQpISGhyvHy6Xl5ec5p48aN06uvvqqRI0fq4Ycf1pdffqm//vWvmjt3brXbKS4uVnFxsfN1+S0PCgsLa70PVTlffNor6w1E3vp/AADewjHedd46xpev1zAMr6zf6uoqP0l1n6HgOv4fAEDg8sYxPtjzkxS4n0HBBP4fAEDgssBnUAHVvDt58qQkKTIyssrxhg0bSpJOnDjhnNa0aVN99tlnGj9+vIYMGaLmzZtr9uzZGjVqVLXbmTlzpqZPn15peps2bWpTPjwg9hVfVwAA8BZvH+NPnDih2NhY727EguoqP0lkKCsLxr/7ABAsvHmMD9b8JPEZFCQF6d99AAgKXj7Gu5KhAqp5564ePXpo48aNLs8/adIkTZgwwfn6/PnzOnbsmBo3biybzeaNEi2nsLBQbdq00f79+xUTE+PrcgAAHhRsx3jDMHTixAm1atXK16X4FbP5SSJDBdvvFgAEk2A7xpOf3MdnUOYF2+8XAASTYDvGm8lQAdW8i4qKkiSdPl31LchOnTolSYqOjq7VdsLDwxUeHl5hWlxcXK3W6a9iYmKC4pcKAIJRMB3jg/WKcanu8pNEhioXTL9bABBsgukYH8z5SeIzKF8Ipt8vAAg2wXSMdzVDhXi5jjqVmJgoSTpw4ECV4+XT27ZtW2c1AQAAWBn5CQAAwDwyFAAA8KaAat51795dkrR9+/Yqx8und+vWrc5qAgAAsDLyEwAAgHlkKAAA4E0B1bzr27evYmNj9f3332vHjh2VxhcvXixJGjp0aB1XFnjCw8P11FNPVbp1AwDA/3GMDy7kp7rD7xYABC6O8cGHDFV3+P0CgMDFMb56NsMwDF8X4aqIiAgVFxerppInT56sZ555RldffbVWrVqlhg0bSpJmz56tiRMnqn///lq3bl0dVQwAAOBb5CcAAADzyFAAAMCXLN28W7FihdLT052vv/jiCxmGoauuuso5bcqUKRoyZIjz9ZkzZzRgwABlZmaqZcuW6tevn/Ly8pSZmammTZsqIyND7du3r9P9AAAAqCvkJwAAAPPIUAAAwEpCfV1ATfLz85WZmVlp+oXT8vPzK4xFRERo7dq1mjlzphYuXKglS5YoPj5eo0ePVnp6uhISErxeNwAAgK+QnwAAAMwjQwEAACux9DfvYD1FRUWaOXOm3nvvPe3bt0/x8fEaNGiQ0tPT1bp1a1+XBwBww7Zt2/TZZ5/piy++0BdffKGDBw9KUo23CALgOvITAAQmMhTgXWQoAAg85CfX0byDy86cOaPrrrtOGRkZzttB5Obm6osvvuB2EADgx2699VYtXbq00nQiAlB75CcACFxkKMB7yFAAEJjIT64L8XUB8B8zZsxQRkaGUlNTtXv3bi1atEiZmZl66aWXlJ+frzFjxvi6RACAG1JTUzVlyhQtW7ZMhw4dUnh4uK9LAgIG+QkAAhcZCvAeMhQABCbyk+v45h1cUlJSombNmsnhcGj79u3q2bNnhfHu3bsrOztbW7duVe/evX1UJQDAEyIiIlRcXMxVT0AtkZ8AILiQoQDPIEMBQPAgP1WPb97BJZs2bZLD4VCHDh0qhSZJGjFihCRp+fLldV0aAACAJZGfAAAAzCNDAQBA8w4u2rlzpySpV69eVY6XT8/Ozq6zmgAAAKyM/AQAAGAeGQoAAJp3cNG+ffskSQkJCVWOl0/Py8urs5oAAACsjPwEAABgHhkKAACad3DRyZMnJUmRkZFVjjds2FCSdOLEiTqrCQAAwMrITwAAAOaRoQAAoHkHAAAAAAAAAAAAWAbNO7gkKipKknT69Okqx0+dOiVJio6OrrOaAAAArIz8BAAAYB4ZCgAAmndwUWJioiTpwIEDVY6XT2/btm2d1QQAAGBl5CcAAADzyFAAANC8g4u6d+8uSdq+fXuV4+XTu3XrVmc1AQAAWBn5CQAAwDwyFAAANO/gor59+yo2Nlbff/+9duzYUWl88eLFkqShQ4fWcWUAAADWRH4CAAAwjwwFAADNO7goLCxM48ePlySlpaU57y8uSbNnz1Z2drb69++v3r17+6pEAAAASyE/AQAAmEeGAgBAshmGYfi6CPiHM2fOaMCAAcrMzFTLli3Vr18/5eXlKTMzU02bNlVGRobat2/v6zIBACatWLFC6enpztdffPGFDMPQVVdd5Zw2ZcoUDRkyxBflAX6N/AQAgYsMBXgPGQoAAhP5yXWhvi4A/iMiIkJr167VzJkztXDhQi1ZskTx8fEaPXq00tPTlZCQ4OsSAQBuyM/PV2ZmZqXpF07Lz8+vy5KAgEF+AoDARYYCvIcMBQCBifzkOr55BwAAAAAAAAAAAFgEz7wDAAAAAAAAAAAALILmHQAAAAAAAAAAAGARNO8AAAAAAAAAAAAAi6B5BwAAAAAAAAAAAFgEzTsAAAAAAAAAAADAImjeAQAAAAAAAAAAABZB8w4AAAAAAAAAAACwCJp3AAAAAAAAAAAAgEXQvAMAAAAAAAAAAAAsguYdgIBx6tQpzZ49W9ddd52aN2+usLAwNWrUSKmpqZo6dar27dvnnHfatGmy2Wx68803fVcwAACAj5GfAAAAzCNDAfC2UF8XAACesHnzZt1+++06fPiwIiMj1adPHzVv3lwOh0NZWVnKyMjQ888/r48//lg33HCDr8sFAADwOfITAACAeWQoAHWB5h0Av7djxw4NHDhQZ86c0eOPP64pU6aoYcOGzvHz589ryZIleuyxx3TgwAEfVgoAAGAN5CcAAADzyFAA6grNOwB+zTAMjRo1SmfOnNG0adP01FNPVZonJCREw4cP18CBA7V//34fVAkAAGAd5CcAAADzyFAA6hLPvAPg11auXKmvv/5aCQkJevLJJ2ucNzY2VpdffnmN8+zZs0fTpk1TamqqWrRoobCwMCUkJOi+++7T7t27q1wmLy9PDz74oDp37qzIyEjFx8frsssu09ixY/Xdd99VmPfrr7/WL3/5S7Vv314RERFq2rSpevTooYcffliHDh0yt/MAAABuID8BAACYR4YCUJdo3gHwaytWrJAk3XHHHQoNrf2XiV9//XU9/fTTOnXqlFJSUnTLLbcoJiZGCxYsUEpKirKzsyvMv3//fvXq1UuvvfaaJGnw4MHq37+/wsPD9fe//11btmxxzrtt2zalpKTonXfeUXR0tIYNG6Y+ffro7Nmz+tOf/lQpZAEAAHgD+QkAAMA8MhSAusRtMwH4tR07dkiSevXq5ZH13XrrrRo7dqzatWtXYfq8efM0ZswYPfzww1qzZo1z+uuvv65jx45p/Pjx+vOf/1xhmX379uns2bPO16+++qrOnDmjF198URMnTqww765duxQbG+uRfQAAAKgJ+QkAAMA8MhSAukTzDoBf++mnnyRJTZs29cj6+vTpU+X0X/3qV3rjjTe0bt06ORwOZ8jJz8+XJN1www2VlklMTKzwuqZ5L7300lrVDQAA4CryEwAAgHlkKAB1ieYdAPzMyZMntXz5cu3YsUPHjh1zXrl06NAhGYah77//3nmVVe/evSVJf/zjH1WvXj3dcMMNioiIqHK9vXv31qeffqq0tDTNmDFD11xzjUduswAAAOBr5CcAAADzyFAAqsNvLAC/1rhxY0n/vaKottasWaO77rqrxvWdOHHC+efRo0dr1apVev/99zV06FBFREQoJSVFgwYN0pgxY9SiRQvnvI8++qg2btyodevW6brrrlNUVJRSU1M1ZMgQjR49mlsWAACAOkF+AgAAMI8MBaAuhfi6AACojR49ekiStm/fXut1nTx5UiNHjtTRo0c1depU5eTk6NSpUzp//rwMw9Ddd98tSTIMw7lMvXr1tGjRIm3fvl1PPfWUUlJSlJmZqSeffFKdO3fW5s2bnfPGxMRozZo12rBhgx577DElJydrzZo1evjhh3XJJZfo3//+d633AQAA4GLITwAAAOaRoQDUJZp3APzakCFDJEkffPCBzp07V6t1bdiwQT/99JNuv/12TZ8+XV26dFFkZKRsNpsk6Ycffqh22Z49e2ratGn6/PPPlZ+fr0ceeUQnTpzQww8/XGE+m82ma665Rs8995wyMzP1448/6u6779aRI0f05JNP1qp+AAAAV5CfAAAAzCNDAahLNO8A+LVBgwbpsssu04EDB/TMM8/UOG9hYaG++eabasePHz8uSUpISKg0tmfPHpevrIqJidHMmTNls9n09ddf1zhvs2bNNG3aNEm66LwAAACeQH4CAAAwjwwFoC7RvAPg12w2m95++21FRERo2rRpmjRpkk6dOlVhHsMwtGzZMl1xxRXKysqqdl2dO3eWJP3jH/+ocL/xgoICPfDAA86HBl9owYIFVQaeTz/9VIZhqE2bNs5pr732mvbu3Vtp3k8++USSKswLAADgLeQnAAAA88hQAOpSqK8LAIDa6tGjh1avXq3bb79ds2bN0quvvqrU1FQ1b95cDodDW7du1ZEjRxQREVFjOLniiit044036rPPPlPnzp01YMAASdK6devUpEkTDRs2TEuXLq2wzIcffqj77rtPHTp0UNeuXdWgQQPt3btXmZmZCgkJ0YwZM5zzvvbaa3rwwQeVnJysLl26KDQ0VLt27dLOnTsVERGhqVOneuX9AQAA+DnyEwAAgHlkKAB1hW/eAQgIffv21Z49e/Tiiy8qJSVF2dnZev/997Vp0yYlJSXpqaee0r///W8NHDiwxvUsXbpUTz75pJo2bapPP/1U27Zt01133aWMjAzFxcVVmn/ChAlKS0tTdHS0NmzYoI8++kj/+c9/dOeddyozM1N33HGHc9709HSNGTNGNptN//rXv7R8+XIVFRXp17/+tXbs2KG+fft6+m0BAACoFvkJAADAPDIUgLpgMwzD8HURAAAAAAAAAAAAAPjmHQAAAAAAAAAAAGAZNO8AAAAAAAAAAAAAi6B5BwAAAAAAAAAAAFgEzTsAAAAAAAAAAADAImjeAQAAAAAAAAAAABZB8w7/rz07FgAAAAAY5G89jR2lEQAAAAAAABPyDgAAAAAAACbkHQAAAAAAAEzIOwAAAAAAAJiQdwAAAAAAADAh7wAAAAAAAGBC3gEAAAAAAMCEvAMAAAAAAICJAL5om9cCvrN8AAAAAElFTkSuQmCC", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plt.rcParams['font.size']=15\n", "f = plt.figure(figsize=(18, 5))\n", "\n", "ax = f.add_subplot(1,3,1)\n", "ax.hist(train_test_data[\"train\"]['Class'], bins = [-0.1, 0.1, 0.9, 1.1], log=True)\n", "ax.set_ylabel(\"Log Counts\")\n", "ax.set_xticks([0, 1])\n", "ax.set_xticklabels([\"0\", \"1\"])\n", "ax.set_xlabel(\"Class\")\n", "ax.set_title(\"Train:\\nDistribution of Classes\")\n", "ax.set_ylim([1, 1e6])\n", "\n", "ax = f.add_subplot(1,3,2)\n", "ax.hist(train_test_data[\"dev\"]['Class'], bins = [-0.1, 0.1, 0.9, 1.1], log=True, color='k')\n", "ax.set_ylabel(\"Log Counts\")\n", "ax.set_xticks([0, 1])\n", "ax.set_xticklabels([\"0\", \"1\"])\n", "ax.set_xlabel(\"Class\")\n", "ax.set_title(\"Dev:\\nDistribution of Classes\")\n", "ax.set_ylim([1, 1e6])\n", "\n", "\n", "ax = f.add_subplot(1,3,3)\n", "ax.hist(train_test_data[\"test\"]['Class'], bins = [-0.1, 0.1, 0.9, 1.1], log=True, color='r')\n", "ax.set_ylabel(\"Log Counts\")\n", "ax.set_xticks([0, 1])\n", "ax.set_xticklabels([\"0\", \"1\"])\n", "ax.set_xlabel(\"Class\")\n", "ax.set_title(\"Test:\\nDistribution of Classes\")\n", "ax.set_ylim([1, 1e6])\n", "\n", "f.tight_layout()" ] }, { "cell_type": "markdown", "id": "operational-binary", "metadata": {}, "source": [ "As you can see, our splitter has greatly increased the representation of the minority class within the training data, but not so for the testing or dev data.\n", "\n", "We hope this tutorial was informative on how to introduce a **custom splitter method** to your datasets! For more customization tutorials, please check our [documentation](https://lightwood.io/tutorials.html).\n", "\n", "If you want to download the Jupyter-notebook version of this tutorial, check out the source github location found here: `lightwood/docssrc/source/tutorials/custom_splitter`. " ] } ], "metadata": { "interpreter": { "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" }, "kernelspec": { "display_name": "Python 3.8.10 64-bit", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.19" } }, "nbformat": 4, "nbformat_minor": 5 }