├── .gitignore ├── README.md ├── docker_setup.sh ├── imgs ├── JupyterLabInterface.png ├── JupyterLabURL.png ├── chapter5notebook.png └── docker_shell_run.png └── notebooks ├── appendixA ├── Appendix_A.ipynb └── Appendix_A_intro.ipynb ├── ch05 └── Chapter5.ipynb ├── ch06 └── Chapter6.ipynb ├── ch07 ├── Chapter7_1.ipynb └── Chapter7_2.ipynb ├── ch08 ├── Chapter8_1.dbc ├── Chapter8_1.html └── Chapter8_1.ipynb ├── ch09 ├── CleanCode.py ├── UnitTestExample.py ├── WoT.py └── __init__.py ├── ch10 └── Chapter10_1.ipynb ├── ch11 └── Chapter11.ipynb ├── ch12 └── Chapter12.ipynb ├── ch13 ├── Chapter_13.dbc ├── Chapter_13.html └── Chapter_13.scala ├── ch14 └── Chapter14.ipynb ├── ch15 ├── Chapter15_1.ipynb └── Chapter15_2.ipynb └── ch16 ├── Chapter16_1.dbc ├── Chapter16_1.html ├── Chapter16_1.ipynb ├── Chapter16_2.dbc ├── Chapter16_2.html └── Chapter16_2.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ML-Engineering 2 | Reference code base for ML Engineering in Action, Manning Publications 3 | Author: Ben Wilson 4 | 5 | 6 | ### About this repo 7 | This is a companion to the Manning book Machine Learning Engineering in Action. 8 | Within this repo are two separate types of notebooks, linked to the examples shown in chapters within the book. 9 | The formats of these notebooks come in several different flavors, depending on the type of examples that they are covering: 10 | - Jupyter notebooks for 'standalone Python' 11 | - PySpark Databricks archive notebooks (these can be imported into Databricks or Databricks Community Edition (free of charge)) 12 | - PySpark html notebook representations (these can be loaded into any web browser for visualization) 13 | - Scala Spark Databricks notebooks (in .dbc, .html, and pure .scala formats) 14 | 15 | For the Jupyter notebooks, a pre-configured bash script is provided at the root level of this directory that will generate a docker image and automatically start the created container for you to rapidly get started with these notebooks. 16 | 17 | 18 | ### Getting Started with Docker 19 | To utilize the pre-built environment and follow along with the examples in the book with additional notes and code that wasn't included in the book, we first need Docker. 20 | 21 | There are a number of different ways to acquire Docker. Please visit their [website](https://docs.docker.com/get-docker/) for instructions on installing the desktop GUI and the engine. 22 | 23 | #### Creating the image 24 | The file [here](/docker_setup.sh) will, when executed through a bash command in your linux terminal, create the container to execute the Jupyter notebooks in this repo. 25 | The script will link this repo to the docker environment through piped synchronization to your local machine, download the required data to execute the code in the notebooks, and 26 | install the necessary dependencies to get Jupyter working (as well as some required libraries that are not part of the Anacondas runtime). 27 | ```text 28 | NOTE: Within the bash script is a variable named 'port' that will allow you to customize 29 | the access port that Jupyter and Docker will use to allow you to utilize a Jupyter notebook 30 | from your local web browser. If you currently have Jupyter running on your machine with the 31 | general 'default port' of 8888, this configuration utilizes 8887. Feel free to change it 32 | if there is a conflict. 33 | ``` 34 | Once the shell script is executed, as shown below, the container will be constructed for your use. 35 | 36 | ![Running the bash script](./imgs/docker_shell_run.png?raw=true) 37 | 38 | #### Getting Jupyter to start up 39 | At the end of the container creation process, your terminal will have a url that you can paste 40 | into your web browser of choice, as shown below. 41 | 42 | ![JupyterLab URL](./imgs/JupyterLabURL.png) 43 | 44 | After copying one of these URL's (I typically stick to the local host 127.0.0.1/ one), paste it 45 | into a browser. You'll have all of the notebooks available that are part of the chapters of 46 | ML Engineering in Action. 47 | 48 | ![JupyterLab Interface](./imgs/JupyterLabInterface.png) 49 | 50 | Navigating within each of the chapters (the ones that have supported standalone Python Jupterlab 51 | examples; the Spark ones will not load here!) will give you the notebook links that you can click 52 | on and open the notebook in a new tab for reading, running, modification, and anything else 53 | you'd like to do. 54 | 55 | ![Example Notebook](./imgs/chapter5notebook.png) -------------------------------------------------------------------------------- /docker_setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # A shell script to initialize a new docker container that will host jupyter notebooks with the conda environment. 3 | # Many thanks to Jas Bali for making this so much better than what I originally had here. -Ben 4 | set -x 5 | port=8887 6 | dataset_repo_folder="$(mktemp -d)/tmp-datasets-folder" 7 | dataset_folder="$dataset_repo_folder/datasets" 8 | final_dataset_folder="$PWD/notebooks/TCPD/datasets" 9 | mkdir -p $dataset_repo_folder 10 | 11 | echo "Cloning datasets into folder: $dataset_repo_folder" 12 | git clone https://github.com/alan-turing-institute/TCPD $dataset_repo_folder 13 | 14 | rm -r $final_dataset_folder 15 | echo "Copying datasets from $dataset_folder into $final_dataset_folder" 16 | mkdir -p $final_dataset_folder 17 | cp -r "$dataset_folder/" "$final_dataset_folder/" 18 | 19 | echo "Starting Jupyter notebooks"s 20 | 21 | docker run -i --name=MLEngineeringInAction \ 22 | -v $(PWD)/notebooks:/opt/notebooks -t \ 23 | -p $port:$port continuumio/anaconda3 bin/bash \ 24 | -c "/opt/conda/bin/conda install jupyter -y --quiet && \ 25 | /opt/conda/bin/conda install -c conda-forge hyperopt=0.2.5 -y --quiet && \ 26 | mkdir -p /opt/notebooks && \ 27 | /opt/conda/bin/jupyter notebook --notebook-dir=/opt/notebooks \ 28 | --ip='*' --port=$port --no-browser --allow-root" 29 | -------------------------------------------------------------------------------- /imgs/JupyterLabInterface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenWilson2/ML-Engineering/0fc05f4b876b26bbacc85bcb11c7c2aef517cd20/imgs/JupyterLabInterface.png -------------------------------------------------------------------------------- /imgs/JupyterLabURL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenWilson2/ML-Engineering/0fc05f4b876b26bbacc85bcb11c7c2aef517cd20/imgs/JupyterLabURL.png -------------------------------------------------------------------------------- /imgs/chapter5notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenWilson2/ML-Engineering/0fc05f4b876b26bbacc85bcb11c7c2aef517cd20/imgs/chapter5notebook.png -------------------------------------------------------------------------------- /imgs/docker_shell_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenWilson2/ML-Engineering/0fc05f4b876b26bbacc85bcb11c7c2aef517cd20/imgs/docker_shell_run.png -------------------------------------------------------------------------------- /notebooks/ch07/Chapter7_1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Chapter 7, ML Engineering\n", 8 | "##### Author: Ben Wilson\n", 9 | "\n", 10 | "In this notebook, we'll be following along with the code listings shown in Chapter 7 for the stand-alone (local VM) portion. This covers section 7.1, listings 7.1 through 7.3.
\n", 11 | "For the remainder of the listing references in section 7.1, you can refer to the project notebook for the local VM implementation for hyperopt tuning of the forecasting problem in the companion notebook to this entitled, \"Chapter7_Local_Hyperopt_Forecasting_Notebook\". That notebook will serve as a full-implementation guide to the remainder of the code listings in section 7.1." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from matplotlib import pyplot as plt\n", 21 | "import random\n", 22 | "from hyperopt import hp, tpe, Trials, fmin" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "### Listing 7.1 Hyperopt fundamentals: the objective function\n", 30 | "In these first 3 listings, we're going to take a look at what hyperopt is actually doing and how it's a bit different from other implementations of hyperparameter tuning. We'll be comparing how the other algorithms (Random Search and Grid Search) fare against hyperopt from an accuracy standpoint and see what comes of our results.
\n", 31 | "To start off, we need a function to optimize. Listing 7.1 below is building a very simple 4th order polynomial that will, based on a value of x that is passed in, provide a loss metric in the form of a reduction factor to the 'y' value based on the submitted x. " 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 2, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "import numpy as np\n", 41 | "def objective_function(x):\n", 42 | " func = np.poly1d([1, -3, -88, 112, -5])\n", 43 | " return func(x) * 0.01\n" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "For fun, let's see what this equation yields if we plot it in the space of [-100:100]" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 3, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "# Get a sorted list between -100 and 100 at every 0.1 increment\n", 60 | "x_values_big = np.arange(-100, 100, 0.1)\n", 61 | "\n", 62 | "# Get the y value for the x values defined above using list comprehension shorthand\n", 63 | "y_values_big = [objective_function(x) for x in x_values_big]\n", 64 | "\n", 65 | "# For those of you who prefer lambda calculus... (and more efficient execution)\n", 66 | "y_values_lambda_big = (lambda x: objective_function(x))(x_values_big)\n", 67 | "\n", 68 | "# Just to placate anyone who wonders if they do the same thing.\n", 69 | "np.testing.assert_array_equal(np.array(y_values_big), y_values_lambda_big)" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "Ok, hold on... what's with the lambda stuff? Who wants functional programming concepts and partial functions in their code base for ML?

\n", 77 | "Professional ML Engineers do.
\n", 78 | "That is, when it's called for.
\n", 79 | "... and here's why." 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": 4, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "# let's make a few more x values here...\n", 89 | "big_x_test = np.arange(-100, 100, 0.0001)" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 5, 95 | "metadata": {}, 96 | "outputs": [ 97 | { 98 | "name": "stdout", 99 | "output_type": "stream", 100 | "text": [ 101 | "Let's make this many: 2000000\n" 102 | ] 103 | } 104 | ], 105 | "source": [ 106 | "print(\"Let's make this many: {}\".format(len(big_x_test)))" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 6, 112 | "metadata": {}, 113 | "outputs": [ 114 | { 115 | "name": "stdout", 116 | "output_type": "stream", 117 | "text": [ 118 | "CPU times: user 1min 18s, sys: 298 ms, total: 1min 18s\n", 119 | "Wall time: 1min 18s\n" 120 | ] 121 | } 122 | ], 123 | "source": [ 124 | "%time list_comp_test = [objective_function(x) for x in big_x_test]" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": 7, 130 | "metadata": {}, 131 | "outputs": [ 132 | { 133 | "name": "stdout", 134 | "output_type": "stream", 135 | "text": [ 136 | "CPU times: user 38.9 ms, sys: 33.8 ms, total: 72.7 ms\n", 137 | "Wall time: 72.6 ms\n" 138 | ] 139 | } 140 | ], 141 | "source": [ 142 | "%time lambda_test = (lambda x: objective_function(x))(big_x_test)" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "That's... quite the difference.
\n", 150 | "Feel free to play around with the value specified in the definition for big_x_test and see if you can melt your CPU and see the non-linear relationship in performance between a list comprehension and a lambda.
\n", 151 | "Note: There are many times that a list comprehension will actually out-perform lambda tasks. But in this particular case, where we're effectively mapping over a collection and calling a function, the optimizations of how lambda handles the traversal with the numpy array (and the subsequent compiled C++ code at the heart of numpy) means that operating on the numpy array with a lambda is going to be MUCH faster than traversing a Python list with a comprehension. These differences only really come into play when you're doing large-scale operations such as this." 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 8, 157 | "metadata": {}, 158 | "outputs": [ 159 | { 160 | "name": "stdout", 161 | "output_type": "stream", 162 | "text": [ 163 | "CPU times: user 434 ms, sys: 70.7 ms, total: 505 ms\n", 164 | "Wall time: 502 ms\n" 165 | ] 166 | }, 167 | { 168 | "data": { 169 | "text/plain": [ 170 | "[]" 171 | ] 172 | }, 173 | "execution_count": 8, 174 | "metadata": {}, 175 | "output_type": "execute_result" 176 | }, 177 | { 178 | "data": { 179 | "image/png": "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\n", 180 | "text/plain": [ 181 | "
" 182 | ] 183 | }, 184 | "metadata": { 185 | "needs_background": "light" 186 | }, 187 | "output_type": "display_data" 188 | } 189 | ], 190 | "source": [ 191 | "# Evaluate the wall-clock speed of plotting the list comprehension\n", 192 | "%time plt.plot(big_x_test, list_comp_test)" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 9, 198 | "metadata": {}, 199 | "outputs": [ 200 | { 201 | "name": "stdout", 202 | "output_type": "stream", 203 | "text": [ 204 | "CPU times: user 71 ms, sys: 44 ms, total: 115 ms\n", 205 | "Wall time: 116 ms\n" 206 | ] 207 | }, 208 | { 209 | "data": { 210 | "text/plain": [ 211 | "[]" 212 | ] 213 | }, 214 | "execution_count": 9, 215 | "metadata": {}, 216 | "output_type": "execute_result" 217 | }, 218 | { 219 | "data": { 220 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXQAAAEDCAYAAAAlRP8qAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nO3deXxcdb3/8ddnJkuztEmzdEvSJV3pXhpK2fddqSgoVa8biqhc9aFewYd38+H9uV69XhREXC7ivcKFCwpCoWyytoWG7nvTNjRJ0zZtmqRNmnW+vz9mUqchaaftzJyZyfv5eIRkzpxk3pyk75x8z/ecY845REQk+fm8DiAiItGhQhcRSREqdBGRFKFCFxFJESp0EZEUoUIXEUkRnha6mf3OzPab2YYI1/+wmW0ys41m9sdY5xMRSSbm5Tx0M7sYOAI85JybeZJ1JwOPApc75w6Z2Qjn3P545BQRSQae7qE7514DGsOXmdlEM3vOzN4xs9fNbFroqc8B9zrnDoU+V2UuIhImEcfQHwD+3jk3H/gGcF9o+RRgipm9aWYrzOxazxKKiCSgNK8DhDOzXOB84DEz612cGXqfBkwGLgVKgdfNbKZzrineOUVEElFCFTrBvxianHNz+3muFljhnOsCdpnZVoIFvzKeAUVEElVCDbk451oIlvUtABY0J/T0n4HLQsuLCA7B7PQkqIhIAvJ62uLDwHJgqpnVmtltwMeA28xsLbARWBRafSlw0Mw2AX8F/sE5d9CL3CIiicjTaYsiIhI9CTXkIiIip8+zg6JFRUVu/PjxXr28iEhSeueddw4454r7e86zQh8/fjyVlZVevbyISFIys3cHek5DLiIiKUKFLiKSIlToIiIpQoUuIpIiVOgiIilChS4ikiJU6CIiKSLpCn3r3sN8f8lmWju6vY4iIpJQkq7Qaxrb+NVrO9lU3+J1FBGRhHLSQj/ZjZxDl7i9x8yqzGydmZ0d/Zh/M6s0D4D1tc2xfBkRkaQTyR76g8CJbvd2HcEbTUwGbgd+eeaxBjZy2BCKh2ayoU6FLiIS7qSF3t+NnPtYBDzkglYA+WY2OloB+zO7JI/1KnQRkeNEYwy9BKgJe1wbWvYeZna7mVWaWWVDQ8Npv+DMkjyqGo7owKiISJhoFLr1s6zfu2Y45x5wzlU45yqKi/u9+mNEZpXk4Rw6MCoiEiYahV4LlIU9LgX2ROHrDkgHRkVE3isahf4U8InQbJeFQLNzrj4KX3dAOjAqIvJeJ73BRehGzpcCRWZWC/wLkA7gnLsfWAJcD1QBbcCnYxU23CwdGBUROc5JC905t/gkzzvgS1FLFKGZJXm8snU/bZ3dZGd4duMlEZGEkXRnivaaXZJHwMGmPTowKiLJoyfQ75yRqEjaQj92YFTDLiKSJJxznPf9l/iPF7bF5OsnbaH3HhhVoYtIsqg9dJT9hzsoHpoZk6+ftIUOwQOjmukiIsliTU0TAHPL8mPy9ZO60GeW5FG1/whtnTpjVEQS37raJjLSfEwdNTQmXz+pC32WDoyKSBJZW9vMjDHDSPfHpnqTvtBBB0ZFJPH1BBwb6pqZUxqb4RZI8kIfOSwzeGBUlwAQkQQXHB7uYU5ZXsxeI6kL3cyYU5rPmtomr6OIiJzQ2tAB0dnaQx/YvLH57Gxopbmty+soIiIDWlvbxNDMNCYU5sTsNZK+0Hun/6zVXrqIJLC1tU3MLsvD5+vviuPRkfSFPqs0D7O/ze8UEUk07V09bKk/HNPhFkiBQh82JJ2JxbnHxqdERBLNpvoWugMupjNcIAUKHYLDLmtqmghe+FFEJLGsC+1wxnKGC6RQoR9s7aT20FGvo4iIvMfa2mZGDM1k1LAhMX2dlCl0gNUadhGRBLS2tonZpfmYxe6AKKRIoU8dNZTMNJ/G0UUk4TQf7WJnQytzSmM73AIpUujpfh+zSvI000VEEk7vFWFnx+gKi+FSotAB5pTls6Guma6egNdRRESOWfXuIQDmxniGC6RQoc8ty6ejO8CW+sNeRxEROead3YeYPCKXvOz0mL9WShU6oOu6iEjCCAQcq3c3cfbY4XF5vZQp9NLhWRTlZrBmtwpdRBLDzgOtNB/t4uxxsR9ugRQq9GNXXqw55HUUEREAVu0O9pH20E/DvLH57NCVF0UkQazefYhhQ9KYWJwbl9dLqUI/e1zwt2Dvb0URES+tereJuWOHx/QKi+FSqtDnluXj9xmV7zZ6HUVEBrmW9i627T/M/DgNt0CKFXp2RhrTRw/jnXe1hy4i3lqzuwnniNsBUUixQgeYP244a2qadIKRiHhq1e5DmP1tSnU8pFyhV4wfTntXgE17WryOIiKD2KrdTUwZMZShQ2J/QlGvlCv0+aEDoxp2ERGvBE8oOhTX4RaIsNDN7Foz22pmVWZ2dz/P55nZX8xsrZltNLNPRz9qZEbnZVGSn6VCFxHP7Gg4wuH2bubF8YAoRFDoZuYH7gWuA6YDi81sep/VvgRscs7NAS4FfmJmGVHOGrH544ZT+W6j7mAkIp6I9wlFvSLZQ18AVDnndjrnOoFHgEV91nHAUAtevT0XaAS6o5r0FMwfN5x9LR3UNekORiISf5XVh8jPTqe8KCeurxtJoZcANWGPa0PLwv0COAvYA6wHvuKce880EzO73cwqzayyoaHhNCOfnMbRRcRLK6sbqRgXvxOKekVS6P0l6juWcQ2wBhgDzAV+YWbD3vNJzj3gnKtwzlUUFxefcthITRs1lOwMvwpdROJu/+F2qg+2sWBCQdxfO5JCrwXKwh6XEtwTD/dp4AkXVAXsAqZFJ+KpS/P7mDc2n8pqFbqIxNfKXcHeOWd8Yhb6SmCymU0IHei8FXiqzzq7gSsAzGwkMBXYGc2gp2r+uAK27G3hSIdnQ/kiMgitrG4kK93PzJLY30O0r5MWunOuG7gTWApsBh51zm00szvM7I7Qat8Fzjez9cBLwF3OuQOxCh2J+eOGE3DBq52JiMTL27samTc2n3R//E/zSYtkJefcEmBJn2X3h328B7g6utHOzNlj8/FZcONeNDl24/UiIr2aj3axeW8LX758sievn3JnivYaOiSdmSV5vLVLV14UkfhY9e4hnINzPTggCilc6BDcqGtqmmjv6vE6iogMAm9XN5Lms7ifIdorxQu9kM7uAGtqdJ9REYm9lbsamVmSR1aG35PXT+lCP2d8AWbw1k4Nu4hIbLV39bCuttmT+ee9UrrQ87LTmTZqGG/tOuh1FBFJcWtrmujsCXgy/7xXShc6BMfRV+0+RGe3bnghIrGzsjo4ElAxzpvxcxgEhb6wvID2rgDrajWOLiKx89auRqaOHMrwHM8uNJv6hb5gQiGApi+KSMx09QR4591Dno6fwyAo9IKcDKaMzFWhi0jMrKttoq2zh/MnFnqaI+ULHYLTF9+pbqRbN44WkRhYVhWceLGwXIUec+eWF9Da2cMG3ThaRGJg+c6DnDV6mKfj5zBICr13XOutnZq+KCLR1d7VQ+W7hzwfboFBUugjhg6hvDiH5Sp0EYmy1bub6OwOcJ7Hwy0wSAod4IKJRby9q1Hz0UUkqpbvOIDPYEG5tzNcYDAV+qQi2jp7WKv56CISRct2HGRWaT7DhqR7HWXwFPp55YX4DN7Y7ul9N0QkhbR1drOmpikhhltgEBV6XnY6s0ryWLZDhS4i0bGy+hDdAZcQB0RhEBU6wPmTili9u4lW3WdURKJg+Y6DpPuNivHeXb8l3KAq9AsnFdEdcLyts0ZFJAqW7zjA3LJ8sjMiuptnzA2qQp8/bjgZaT7eqNKwi4icmeajXayva06Y8XMYZIU+JN3POeOH86YKXUTO0PIdBwg4uGhK4tyEflAVOgSnL27Ze5gDRzq8jiIiSey17QfIzUxjblm+11GOGXyFPrEICM4dFRE5Hc45XtvWwHkTC0n3J06NJk6SOJlZksewIWm8qfnoInKaqg+2UXvoKBcn0HALDMJC9/uM8yYW8kbVAZxzXscRkST0+vYGAC6eXORxkuMNukIHuGhyMXVNR9l5oNXrKCKShF7b1sDYgmzGFeZ4HeU4g7LQLwn9mfTK1gaPk4hIsunsDrB8x0EuSrC9cxikhV5WkM3E4hxe2brf6ygikmRW7z5Ea2dPwo2fwyAtdIBLp47grV2NHO3s8TqKiCSR17Y3HDsWl2gGcaEX09kdYIVueiEip+D17QeYV5YYl8vtK6JCN7NrzWyrmVWZ2d0DrHOpma0xs41m9mp0Y0bfOeMLyEr38+o2jaOLSGQaWztZX9fMRZMTb7gF4KRXlDEzP3AvcBVQC6w0s6ecc5vC1skH7gOudc7tNrMRsQocLUPS/Zw3sTA0jj7D6zgikgRe396Ac3DxlMQ7IAqR7aEvAKqcczudc53AI8CiPut8FHjCObcbwDmXFEcbL5lSTPXBNqo1fVFEIvDS5v0U5mQwpzRxTvcPF0mhlwA1YY9rQ8vCTQGGm9krZvaOmX2ivy9kZrebWaWZVTY0eD/UcenU4J9NGnYRkZPp7gnw6rYGLp06Ap/PvI7Tr0gKvb/kfU+xTAPmAzcA1wD/ZGZT3vNJzj3gnKtwzlUUF3s/BjWuMIcJRZq+KCInt2p3E81Hu7jirMQdUY6k0GuBsrDHpcCeftZ5zjnX6pw7ALwGzIlOxNi6ZEoxy3cepL1L0xdFZGAvb9lPms+4MAFPKOoVSaGvBCab2QQzywBuBZ7qs86TwEVmlmZm2cC5wOboRo2NS6YW094VYLmmL4rICby8ZR8LJhQk5HTFXictdOdcN3AnsJRgST/qnNtoZneY2R2hdTYDzwHrgLeB3zjnNsQudvScV15Idoaflzbv8zqKiCSomsY2tu07wuXTEne4BSKYtgjgnFsCLOmz7P4+j38M/Dh60eJjSLqfiycX8+Km/Xx3kcMsMQ92iIh3/ho6zpbohT5ozxQNd+X0kextaWfjnhavo4hIAnp5y37GF2ZTXpzrdZQTUqEDl00txmfwwiYNu4jI8do6u1m24yCXTxvpdZSTUqEDhbmZzB83nBc1ji4ifSyrOkhndyDhh1tAhX7MlWeNZOOeFvY0HfU6iogkkOc37WVoZhoLJhR4HeWkVOghV04P/jmlvXQR6dXdE+DFzfu5/KwRZKQlfl0mfsI4mVicS3lRjsbRReSYyncP0djayTUzRnkdJSIq9DBXTh/Jip0HOdze5XUUEUkAz23YS0aa79htKxOdCj3MlWeNpKvH6WJdIoJzjhc27ePiyUXkZEZ0yo7nVOhh5o8bTlFuBs9t2Ot1FBHx2Ia6FuqajnJ1kgy3gAr9OH6fcfWMUby8Zb8u1iUyyC3duBefBf9yTxYq9D6unzmats4eXtmqYReRwWzpxr0smFBAQU6G11EipkLvY2F5AcOz03l2Q73XUUTEIzsbjrB9/5Gkmd3SS4XeR5rfxzUzRvHSZg27iAxWz20MHkdLpvFzUKH367pZoznS0c0b2w94HUVEPPDMunrmlOVTkp/ldZRTokLvx/kTC8nLSmeJhl1EBp1dB1rZuKeF988e7XWUU6ZC70e638fV00fywqZ9dHRr2EVkMHl6bfAOmzeo0FPH9bNGc7i9m2VVujWdyGDyl3V7OGf8cEbnJddwC6jQB3TBpCKGDknj6XUadhEZLLbtO8y2fUd43+wxXkc5LSr0AWSk+bh2xiiWbtyr2S4ig8TTa/fgM7huVnLNbumlQj+BD8wr4UhHNy9t3u91FBGJMeccT6+rZ2F5ISOGDvE6zmlRoZ9A8BubyZNr6ryOIiIxtqm+hZ0HWpN2uAVU6Cfk9xnvnzOGV7Y20NymS+qKpLK/rK3H7zOunZmcwy2gQj+pD8wtobMnoDnpIiksEHA8uaaOiyYXJdW1W/pSoZ/EzJJhlBfn8OfVGnYRSVUrdh6kvrmdD51d6nWUM6JCPwkzY9GcEt6ubtQNpEVS1OOr6hiamcZV05PnUrn9UaFHYNHcMTgHfwmdQSYiqaOts5tnN9Rzw+zRDEn3ex3njKjQIzC+KIe5Zfn8aXUdzjmv44hIFC3duJe2zh4+mOTDLaBCj9jN80vZsvcwG+pavI4iIlH0xKo6ygqyqBg33OsoZ0yFHqH3zxlDZpqPRytrvI4iIlGyt7mdN6oOcNO8Unw+8zrOGYuo0M3sWjPbamZVZnb3CdY7x8x6zOzm6EVMDHlZ6Vw3cxRPrqnTpQBEUsSf19ThHHxwXonXUaLipIVuZn7gXuA6YDqw2MymD7DeD4Gl0Q6ZKD5cUUZLezdLQ3czEZHk5Zzjscoa5o8bzviiHK/jREUke+gLgCrn3E7nXCfwCLCon/X+HngcSNkLnywsL6R0eBaPVdZ6HUVEztDK6kPsaGjlI+eUeR0laiIp9BIgfOC4NrTsGDMrAW4C7j/RFzKz282s0swqGxoaTjWr53w+45b5Zby54wA1jW1exxGRM/Dw27sZmpnG+5LwRhYDiaTQ+ztS0Hfu3s+Au5xzJxxcds494JyrcM5VFBcXR5oxodxcEZza9Pgq7aWLJKumtk6eWV/PonljyM5I8zpO1ERS6LVA+N8kpUDfM2wqgEfMrBq4GbjPzD4QlYQJpiQ/iwsnFfFYZS09Ac1JF0lGf1pdR2d3gMULxnodJaoiKfSVwGQzm2BmGcCtwFPhKzjnJjjnxjvnxgP/B3zROffnqKdNEIsXjKWu6SivbkvZwwUiKcs5xyNv1zC7NI8ZY/K8jhNVJy1051w3cCfB2SubgUedcxvN7A4zuyPWARPRVdNHMmJoJn9Y/q7XUUTkFK2uaWLrvsMpt3cOENHgkXNuCbCkz7J+D4A65z515rESW7rfx+IFY7nn5e3sPtjG2MJsryOJSIQefms32Rl+3j8neW9kMRCdKXqaFi8Yi8+M/3lbe+kiyeJQaydPrd3Dorkl5GamzsHQXir00zQqbwhXTx/JoytrdOaoSJJ4ZGUNHd0BPnn+OK+jxIQK/Qx8fOE4DrV1sWS97mYkkui6ewL894p3Oa+8kGmjhnkdJyZU6Gfg/ImFlBfn8IcVGnYRSXQvbt5HXdNRPnn+eK+jxIwK/QyYGX+3cByrdzexpqbJ6zgicgIPLqumJD+LK88a4XWUmFGhn6FbKsoYmpnGb17f6XUUERnA5voWVuxs5BPnjSPNn7q1l7r/Z3GSm5nG4nPH8uyGvdQe0vVdRBLRQ8urGZLuS6kLcfVHhR4Fnzp/PAb815vVXkcRkT4aDnfw+Ko6bppXSn52htdxYkqFHgVj8rO4YfZo/ndlDS3tXV7HEZEwv19WTVdPgM9dNMHrKDGnQo+Sz11UzpGObh55e7fXUUQkpLWjm4eWV3PN9FGUF+d6HSfmVOhRMrMkj4XlBfzXm8G9ARHx3sNv76alvZvPX1LudZS4UKFH0e0Xl1Pf3M6Ta/peXVhE4q2rJ8Bv39jFuRMKmDd2uNdx4kKFHkWXTR3B9NHDuO+vVbpWuojHnlqzh/rmdu64ZKLXUeJGhR5FZsbfXz6JnQdaeUaXAxDxTCDguP/VHUwdOZRLpybn3dFOhwo9yq6ZMYrJI3L5xcvbCWgvXcQTSzbUs33/Eb50+STM+ruLZmpSoUeZz2fcefkktu07wvOb9nkdR2TQCQQc//nidiaPyOWGWalzA+hIqNBj4IZZoxlfmM3PX96Oc9pLF4mnZ9YH986/fMVk/L7Bs3cOKvSYSPP7+OJlk9i4p4UXN+u+oyLx0hNw3PPS4Nw7BxV6zHxwXgkTinL496VbNeNFJE6WhPbOv3LlZHyDbO8cVOgxk+b38bWrprB132GeWlvndRyRlNfdE+BnL25jyshcrp85+PbOQYUeUzfMGs2MMcP46Qvb6OzW2aMisfRoZS07Glr5+tVTB+XeOajQY8rnM/7hmqnUNB7lkZW6xotIrLR1dvMfL26jYtxwrp4+0us4nlGhx9glU4pZMKGAe16qoq2z2+s4Iinpt6/vouFwB9+6ftqgmnfelwo9xsyMu66dyoEjHfzqVd3VSCTaDhzp4P5Xd3DNjJHMH1fgdRxPqdDjYP64At43ezS/em0HdU1HvY4jklJ+/tJ22rsDfPPaaV5H8ZwKPU6+df1ZOAc/eHaL11FEUsbWvYf577d2s3hBGRMHwfXOT0aFHicl+Vl8/uJy/rJ2D5XVjV7HEUl6zjn+5akNDB2Sxtevmup1nISgQo+jOy6dyKhhQ/jOXzbpwl0iZ+jpdfWs2NnIN66eyvCc1L5XaKRU6HGUnZHG3ddNY31dM/9bWeN1HJGk1dbZzfeWbGbGmGEsXjDW6zgJQ4UeZ4vmjuHcCQV8f8lm9h9u9zqOSFL6xctV1De3850bZwy6C3CdSESFbmbXmtlWM6sys7v7ef5jZrYu9LbMzOZEP2pqMDO+98FZtHcF+O7Tm72OI5J0tuxt4YHXdvLBs0uoGD+4pyn2ddJCNzM/cC9wHTAdWGxm0/ustgu4xDk3G/gu8EC0g6aSicW5fPGyifxl7R5e2aqrMYpEqifguOvx9QzLSucfb+hbQxLJHvoCoMo5t9M51wk8AiwKX8E5t8w5dyj0cAVQGt2YqecLl05kYnEO//jnDTqDVCRC//XmLtbWNPGvN86gQAdC3yOSQi8Bwo/g1YaWDeQ24Nn+njCz282s0swqGxoaIk+ZgjLT/HzvplnUHjrKj57b6nUckYRX09jGT57fxhXTRvD+2YPzaoonE0mh93fEod85d2Z2GcFCv6u/551zDzjnKpxzFcXFg+fGrQM5t7yQT50/ngeXVbOs6oDXcUQSViDguOvxdfh9xr/dNHNQX6/lRCIp9FqgLOxxKbCn70pmNhv4DbDIOXcwOvFS313XTqO8KIdvPLaWlvYur+OIJKTfvbmLZTsO8o83nMXovCyv4ySsSAp9JTDZzCaYWQZwK/BU+ApmNhZ4Avg759y26MdMXVkZfn76kbnsO9zBd57a5HUckYSzub6FHz23launj+Qj55Sd/BMGsZMWunOuG7gTWApsBh51zm00szvM7I7Qav8MFAL3mdkaM6uMWeIUNLcsny9dOpHHV9Xy7Pp6r+OIJIz2rh6++sga8rLT+cGHZmuo5STSIlnJObcEWNJn2f1hH38W+Gx0ow0ud14+mVe3NfDNx9cxY0weYwuzvY4k4rkfPLuFrfsO8+Cnz9GslgjoTNEEkZHm4xcfPRsDvvTHVXR093gdScRTT6/bw4PLqvn0BeO5dOoIr+MkBRV6AikryObfb5nD+rpm/t8zOotUBq+q/Ue46//WcfbYfL513Vlex0kaKvQEc/WMUXz2wgk8tPxdnlxT53Uckbhr7ejmC//9DkPS/dz7sbPJSFNNRUpbKgHddd00Fowv4Jv/t451tU1exxGJm0DA8c3H17Gj4Qj3LJ6nKYqnSIWegNL9Pn758bMpys3kcw9VsrdZV2WUweFnL27jmXX13HXtNC6YVOR1nKSjQk9QhbmZ/PZTFRxp7+b2P1TS3qWDpJLanlhVyz0vV/HhilJuv7jc6zhJSYWewKaNGsbPbp3H+rpmvvzwarp7Al5HEomJt3c1cvfj6zmvvJB/+8AszTc/TSr0BHfV9JH8y/um8/ymfXz7TxtwTreuk9SyaU8Ln/39SkqHZ/HLj+sg6JmI6MQi8danLpjAwdZOfv5yFQW5Gdx17TSvI4lExa4DrXzid2+Rk5nGQ7ctID9bJw+dCRV6kvjaVVM42NrJL1/ZwbAh6Xzh0oleRxI5I/XNR/n4b94i4OAPt51L6XCdHX2mVOhJwsz47qKZHG7v5ofPbSHgHF+6bJLXsUROS13TUT766xU0H+3i4c8tZNKIXK8jpQQVehLx+4z/+PAcfAY/XrqVnoDjy1dM9jqWyCl592ArH/31W7S0d/H7zyxgVmme15FShgo9yaT5ffz0w3Pxm/HTF7bR3tXDP1wzVbMCJCnsaDjCR3+9go7uAA9/biEzS1Tm0aRCT0J+n/HjW+aQme7jvld2sLelnR98cLZmB0hCq6xu5HMPVeL3GY/cvpBpo4Z5HSnlqNCTlN9nfO+mWYzOy+KnL2yj4XAH933sbIYOSfc6msh7PL1uD197dC0l+Vk8+OlzGFeY43WklKRduiRmZnz5isn86ObZLNtxkA/9chm7DrR6HUvkGOcc9/61ijv/uJo5pXk88YXzVeYxpEJPAR+uKOOhzyyg4XAHN/7iDV7avM/rSCK0tHfx+T+8w4+XbuXGOWP4w23nMlw3qYgpFXqKuGBSEU/deSFjC7K57feV/PT5rbpUgHhmc30LN/78DV7esp9/et90/vPWuQxJ93sdK+Wp0FNIWUE2j3/hfG6eX8o9L1dx8/3LqdYQjMRRIOD4zes7WXTvm7R19vDw7Qu57cIJmoUVJyr0FDMk3c+/3zKHexbPY2fDEa6/53Uefnu3rgEjMVfT2MbiX6/g357ZzCVTilnylYs4Z3yB17EGFc1ySVE3zhlDxbjhfP3RtXzrifX8aXUd37tpJpNGDPU6mqSYrp4AD75Zzc9e3IaZ8eObZ3Pz/FLtlXvAvNpzq6iocJWVlZ689mASCDgerazh+89uoa2zm9svLueLl04iJ1O/y+XMvb2rkX/68wa27jvM5dNG8J0bZ1BWoGuyxJKZveOcq+j3ORX64HDgSAffe2YzT6yuoyg3k69eOZmPnFNGul+jbnLqqvYf5ifPb+PZDXspyc/iX94/naumj9ReeRyo0OWYd949xA+e3czK6kOUF+XwlSsnc8Os0aSp2CUCNY1t3PPSdh5fVUtWup/PXlTO5y8pJztDf/HFiwpdjuOc46XN+/nR0i1s23eEsoIsbr94IrfML9XUMunXhrpmfvXaTpasr8dvxt+dN44vXjqRwtxMr6MNOip06Vcg4Hhx8z7ue2UHa2qaKMjJ4Ob5pSxeMJYJRTqbb7Dr6O7h+Y37+ONbu1m+8yC5mWksXlDGZy6cwOi8LK/jDVoqdDkh5xxv7WrkwTereWHzPnoCjvPKC7l5filXTh9JXpauDzNYOOfYuKeFx1fV8ufVdRxq66IkP4tPnDeOxeeOZZiuFeS5ExW6Br4EM2NheSELywvZ39LOY+/U8sjK3Xz9sbVk+H1cPKWI62eN5rKpI3TqdgoKBBxraptYumEvz27Yy9nTrEYAAAlQSURBVO7GNtL9xtUzRvGRijIumFSE36eDnclAe+jSL+cca2qaeGZdPUvW17OnuR0zmFWSx8WTi7lochFzyvI15p6kahrbeLPqAG9UHWD5joMcbO0k3W+cP7GI62aO4uoZoyjQL++EpCEXOSOBgGNdXTOvbWvgtW0NrK5poifgSPcb08fkMa8sn3lj85kxJo/xhdmaMZNgWtq72FDbzJraJtbVNLOutok9ze0AjByWyQUTi7hoShGXT9PwWjI440I3s2uB/wT8wG+ccz/o87yFnr8eaAM+5ZxbdaKvqUJPXs1Hu3hr50FW7W5i9e5DrKtt5mhXDwAZfh/lxTlMHTWUySNyKR2eTenwLEqHZzNiaCY+/ekeE0c6uqlvOsqe5naqD7Syo+EIVfuPsKPhCPtaOo6tN74wm9ml+Zw9Np8LJxcxsThXc8eTzBmNoZuZH7gXuAqoBVaa2VPOuU1hq10HTA69nQv8MvReUlBeVjpXzwj+WQ7Q3RNg677DbKk/zLZ9wbfK6kM8uWbPcZ+X7jdG5Q2hKDeTwpwMCnMyKcjNoDAng7ysdHIz08jJTCMn0x98nxF8nJnmI81vpPt8KfsLoSfgaO/q4WhXD+3H3gK0d/XQ2tlDU1snTW1dHAq9bz7axcHWTvY1t7On+SiH27uP+3q5mWlMHJHLhZOKmTgih5lj8phdmkd+toZRUlkkB0UXAFXOuZ0AZvYIsAgIL/RFwEMuuLu/wszyzWy0c64+6okl4aT5fcwYk8eMMcffH/JoZw91TW3UHjp67K2++SgHj3RS19TOutpmGls76Q5EPuzn9xnpoXJPT/OR5jPSfHZsL7N3Z9MMDAu9711mwY971wEcwf84gscNgu/B4YLvQ9H6fY7e58MfB9ejd93exyd4jYBzdPVEvg2GDkljeHYG+dnpjCvMZmF5AaPzsxidN4TReVmMKwz+NaQ978EnkkIvAWrCHtfy3r3v/tYpAY4rdDO7HbgdYOzYsaeaVZJMVoafSSOGnvCCYM45Wo5209LexZGOblo7umnt7KG1o/vY466eAF09LvQ+QHePozP0vqsnQHfAHSvI4BcdoFxDy0KrBP8TKvzesrc+j+nzi+G4XxShFfp9LvS64b9o3vN1Qq/h8xlD0vxkZfgYku7/21uaj6wMP9kZfvKzM8jPSicvK13HKGRAkRR6f7/m++5ORLIOzrkHgAcgOIYewWtLijMz8rLTycvWwTiRMxXJr/paoCzscSmw5zTWERGRGIqk0FcCk81sgpllALcCT/VZ5yngExa0EGjW+LmISHyddMjFOddtZncCSwlOW/ydc26jmd0Rev5+YAnBKYtVBKctfjp2kUVEpD8RnfrvnFtCsLTDl90f9rEDvhTdaCIicip0uFxEJEWo0EVEUoQKXUQkRajQRURShGdXWzSzBuDd0/z0IuBAFONES6LmgsTNplynRrlOTSrmGuecK+7vCc8K/UyYWeVAVxvzUqLmgsTNplynRrlOzWDLpSEXEZEUoUIXEUkRyVroD3gdYACJmgsSN5tynRrlOjWDKldSjqGLiMh7JeseuoiI9KFCFxFJEQlf6GZ2i5ltNLOAmVX0ee5bZlZlZlvN7Jqw5fPNbH3ouXssxvfiMrP/NbM1obdqM1sTWj7ezI6GPXf/yb5WlHP9q5nVhb3+9WHP9bvt4pTrx2a2xczWmdmfzCw/tNzT7RXKcG1om1SZ2d3xfv2wHGVm9lcz2xz6+f9KaPmA39M4ZqsO/ftaY2aVoWUFZvaCmW0PvR8e50xTw7bJGjNrMbOverG9zOx3ZrbfzDaELRtw+0T136JzLqHfgLOAqcArQEXY8unAWiATmADsAPyh594GziN4J6VngevimPcnwD+HPh4PbPBw2/0r8I1+lg+47eKU62ogLfTxD4EfJsj28oe2RTmQEdpG0z3KMho4O/TxUGBb6PvW7/c0ztmqgaI+y34E3B36+O7e76mH38e9wDgvthdwMXB2+M/yQNsn2v8WE34P3Tm32Tm3tZ+nFgGPOOc6nHO7CF6LfYGZjQaGOeeWu+AWewj4QDyyhv4S+DDwcDxe7wz0u+3i9eLOueedc723qV9B8A5XieDYDdGdc51A7w3R4845V++cWxX6+DCwmeB9ehPVIuD3oY9/T5z+zQ3gCmCHc+50z0Q/I86514DGPosH2j5R/beY8IV+AgPdmLok9HHf5fFwEbDPObc9bNkEM1ttZq+a2UVxyhHuztDQxu/C/swbaNt54TME/4rq5eX2SqTtcoyZjQfmAW+FFvX3PY0nBzxvZu9Y8MbvACNd6C5lofcjPMjV61aO36nyenvBwNsnqj9zCVHoZvaimW3o5+1Ee0cD3Zg6ohtWxyjjYo7/QaoHxjrn5gFfA/5oZsPONMsp5PolMBGYG8ryk95P6+dLRXX+aiTby8y+DXQD/xNaFPPtdbLY/SzzdF6vmeUCjwNfdc61MPD3NJ4ucM6dDVwHfMnMLvYgQ78seJvMG4HHQosSYXudSFR/5iK6Y1GsOeeuPI1PG+jG1LUc/yd8VG5YfbKMZpYGfBCYH/Y5HUBH6ON3zGwHMAWoPNM8keYKy/dr4OnQw5jf1DuC7fVJ4H3AFaGhsbhsr5NIqJudm1k6wTL/H+fcEwDOuX1hz4d/T+PGObcn9H6/mf2J4BDBPjMb7ZyrDw177o93rpDrgFW92ykRtlfIQNsnqj9zCbGHfpqeAm41s0wzmwBMBt4O/Tlz2MwWhsa0PwE8GYc8VwJbnHPHhnvMrNjM/KGPy0MZd8YhS+/rjw57eBPQe9S9320Xx1zXAncBNzrn2sKWe7q9iOyG6HER+tn9LbDZOffTsOUDfU/jlSvHzIb2fkzwAPcGgtvpk6HVPkl8/s3157i/kr3eXmEG2j7R/bfo1ZHoUzhifBPB32IdwD5gadhz3yZ4VHgrYTNZgAqC37gdwC8InREb45wPAnf0WfYhYCPBo9irgPfHedv9AVgPrAv94Iw+2baLU64qguOGa0Jv9yfC9gpluJ7gjJIdwLfj/fphOS4k+Kf3urDtdP2JvqdxylUe+v6sDX2vvh1aXgi8BGwPvS/wYJtlAweBvLBlcd9eBH+h1ANdoe667UTbJ5r/FnXqv4hIikjmIRcREQmjQhcRSREqdBGRFKFCFxFJESp0EZEUoUIXEUkRKnQRkRTx/wGfMxbUw6FvIQAAAABJRU5ErkJggg==\n", 221 | "text/plain": [ 222 | "
" 223 | ] 224 | }, 225 | "metadata": { 226 | "needs_background": "light" 227 | }, 228 | "output_type": "display_data" 229 | } 230 | ], 231 | "source": [ 232 | "# And evaluate the wall-clock of plotting the lambda implementation\n", 233 | "%time plt.plot(big_x_test, lambda_test)" 234 | ] 235 | }, 236 | { 237 | "cell_type": "markdown", 238 | "metadata": {}, 239 | "source": [ 240 | ".... and for those who will state, \"But lambda and list comprehensions are lazily evaluated in Py3.x....\", let's ensure that they're materialized through a forced execution by plotting their values in pyplot. This comes down the fact that one is a numpy array and one is a list (because the initial calculation to generate the collections above in the first block of timeit cells actually wasn't lazy)." 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": 10, 246 | "metadata": {}, 247 | "outputs": [ 248 | { 249 | "data": { 250 | "text/plain": [ 251 | "[]" 252 | ] 253 | }, 254 | "execution_count": 10, 255 | "metadata": {}, 256 | "output_type": "execute_result" 257 | }, 258 | { 259 | "data": { 260 | "image/png": "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\n", 261 | "text/plain": [ 262 | "
" 263 | ] 264 | }, 265 | "metadata": { 266 | "needs_background": "light" 267 | }, 268 | "output_type": "display_data" 269 | } 270 | ], 271 | "source": [ 272 | "y_lambda = (lambda x: objective_function(x))(x_values_big)\n", 273 | "plt.plot(x_values_big, y_lambda)" 274 | ] 275 | }, 276 | { 277 | "cell_type": "markdown", 278 | "metadata": {}, 279 | "source": [ 280 | "So... that's not super helpful for seeing the nuance to this equation. The resolution of the plot doesn't allow us to see where the actual minimum value is." 281 | ] 282 | }, 283 | { 284 | "cell_type": "markdown", 285 | "metadata": {}, 286 | "source": [ 287 | "Let's see what the representation will be for this equation if we generate values against this function (in the x space before it becomes very large in the y space so that we can see the challenge that these different methods of searching for a global minima will have. This will help to inform our search range for the different approaches as well." 288 | ] 289 | }, 290 | { 291 | "cell_type": "code", 292 | "execution_count": 11, 293 | "metadata": {}, 294 | "outputs": [ 295 | { 296 | "data": { 297 | "text/plain": [ 298 | "[]" 299 | ] 300 | }, 301 | "execution_count": 11, 302 | "metadata": {}, 303 | "output_type": "execute_result" 304 | }, 305 | { 306 | "data": { 307 | "image/png": "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\n", 308 | "text/plain": [ 309 | "
" 310 | ] 311 | }, 312 | "metadata": { 313 | "needs_background": "light" 314 | }, 315 | "output_type": "display_data" 316 | } 317 | ], 318 | "source": [ 319 | "# Now, let's see it zoomed in to the range where we can see what's going on with minimum range of values for y...\n", 320 | "x_axis_values = np.arange(-12.0, 12.0, 0.01).tolist()\n", 321 | "y_values = (lambda x: objective_function(x))(x_axis_values)\n", 322 | "plt.plot(x_axis_values, y_values)" 323 | ] 324 | }, 325 | { 326 | "cell_type": "markdown", 327 | "metadata": {}, 328 | "source": [ 329 | "Now, I know what you might be thinking... (it's what I'd be thinking if someone showed me this as well, likely...)
\n", 330 | "\"But Ben, dude, you can just get the minimum value directly from the data.\"
\n", 331 | "To which I would reply...
\n", 332 | "\"Shhhh... I'm just trying to make a point here. Let's play pretend and think of this as a supervised learning problem where our vector has 17 dimensions and there's no way that our human minds can figure out how to minimize the function.\"
\n", 333 | "But, to humor us both, here's the actual minimum x value." 334 | ] 335 | }, 336 | { 337 | "cell_type": "code", 338 | "execution_count": 12, 339 | "metadata": {}, 340 | "outputs": [ 341 | { 342 | "data": { 343 | "text/plain": [ 344 | "7.569999999999581" 345 | ] 346 | }, 347 | "execution_count": 12, 348 | "metadata": {}, 349 | "output_type": "execute_result" 350 | } 351 | ], 352 | "source": [ 353 | "x_axis_values[np.argmin(y_values)]" 354 | ] 355 | }, 356 | { 357 | "cell_type": "markdown", 358 | "metadata": {}, 359 | "source": [ 360 | "So, how would this look as a grid search problem?

\n", 361 | "We would likely, when trying to do grid search over this space, select some collection of points to try, get the results, and then rank sort the return values, returning the lowest one. (Coincidentally that's exactly how grid search implementations actually work)." 362 | ] 363 | }, 364 | { 365 | "cell_type": "code", 366 | "execution_count": 13, 367 | "metadata": {}, 368 | "outputs": [ 369 | { 370 | "data": { 371 | "text/plain": [ 372 | "'-12, -11, -10, -9, -8.5, -8.0, -7.5, -7.0, -6.5, -6.0, -5.5, -5.0, -4.5, -4.0, -3.5, -3.0, -2.5, -2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 10.0, 11.0, 12.0'" 373 | ] 374 | }, 375 | "execution_count": 13, 376 | "metadata": {}, 377 | "output_type": "execute_result" 378 | } 379 | ], 380 | "source": [ 381 | "# our_grid = [-12, -11, -10, -9, -8.5, -8, ... ok, I'm not typing any more of this.]\n", 382 | "our_grid = np.arange(-12, -8, 1).tolist() + np.arange(-8.5, 9.0, 0.5).tolist() + np.arange(9.0, 13, 1).tolist()\n", 383 | "\", \".join(str(g) for g in our_grid)" 384 | ] 385 | }, 386 | { 387 | "cell_type": "markdown", 388 | "metadata": {}, 389 | "source": [ 390 | "Ok, so we have a grid to search over now. Let's see what the results of calling our function and getting a minima will be for this." 391 | ] 392 | }, 393 | { 394 | "cell_type": "code", 395 | "execution_count": 14, 396 | "metadata": {}, 397 | "outputs": [ 398 | { 399 | "name": "stdout", 400 | "output_type": "stream", 401 | "text": [ 402 | "Our minimum x value for our grid search is: 7.5 based on a y min of -22.165625000000002\n" 403 | ] 404 | } 405 | ], 406 | "source": [ 407 | "grid_search_test = (lambda x: objective_function(x))(our_grid)\n", 408 | "print(\"Our minimum x value for our grid search is: {} based on a y min of {}\".format(\n", 409 | " our_grid[np.argmin(grid_search_test)], grid_search_test.min()))" 410 | ] 411 | }, 412 | { 413 | "cell_type": "markdown", 414 | "metadata": {}, 415 | "source": [ 416 | "It was close. Definitely close. But is it close enough? Let's try a random search to see how that fares.
\n", 417 | "We'll do the same exact search space (-12, 12) and give it 1000 iterations. " 418 | ] 419 | }, 420 | { 421 | "cell_type": "code", 422 | "execution_count": 15, 423 | "metadata": {}, 424 | "outputs": [ 425 | { 426 | "data": { 427 | "text/plain": [ 428 | "(array([ 98., 84., 107., 106., 103., 106., 112., 95., 97., 92.]),\n", 429 | " array([-1.19695019e+01, -9.57358465e+00, -7.17766744e+00, -4.78175022e+00,\n", 430 | " -2.38583301e+00, 1.00842068e-02, 2.40600142e+00, 4.80191864e+00,\n", 431 | " 7.19783585e+00, 9.59375307e+00, 1.19896703e+01]),\n", 432 | " )" 433 | ] 434 | }, 435 | "execution_count": 15, 436 | "metadata": {}, 437 | "output_type": "execute_result" 438 | }, 439 | { 440 | "data": { 441 | "image/png": "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\n", 442 | "text/plain": [ 443 | "
" 444 | ] 445 | }, 446 | "metadata": { 447 | "needs_background": "light" 448 | }, 449 | "output_type": "display_data" 450 | } 451 | ], 452 | "source": [ 453 | "# We're going to use uniform search space here since that's how most people use the default values of a random search\n", 454 | "# for hyperparameter tuning. (There are other options for most APIs though with different distributions)\n", 455 | "random_search_x = [random.uniform(-12.0, 12.0) for x in range(1000)]\n", 456 | "plt.hist(random_search_x)" 457 | ] 458 | }, 459 | { 460 | "cell_type": "markdown", 461 | "metadata": {}, 462 | "source": [ 463 | "And now, let's build the collection and see what happens for optimization." 464 | ] 465 | }, 466 | { 467 | "cell_type": "code", 468 | "execution_count": 16, 469 | "metadata": {}, 470 | "outputs": [ 471 | { 472 | "name": "stdout", 473 | "output_type": "stream", 474 | "text": [ 475 | "Our minimum x value for our random search is: 7.559268444196029 based on a y min of -22.1750297705125\n" 476 | ] 477 | } 478 | ], 479 | "source": [ 480 | "random_search_test = (lambda x: objective_function(x))(random_search_x)\n", 481 | "print(\"Our minimum x value for our random search is: {} based on a y min of {}\".format(\n", 482 | " random_search_x[np.argmin(random_search_test)], random_search_test.min()))" 483 | ] 484 | }, 485 | { 486 | "cell_type": "markdown", 487 | "metadata": {}, 488 | "source": [ 489 | "Wow, that was MUCH closer, right?
\n", 490 | "It would take a bit longer than the grid search (particularly if we were doing something more complicated that basic linear algebra for our 'model') by searching through 1000 iterations.
\n", 491 | "Let's see how many iterations it would take to get something VERY close to the global minima, though.\n", 492 | ">> NOTE: this algorithm for search space is non-deterministic. If you run this more than once, you're likely going to get different answers. That's how random number generators work." 493 | ] 494 | }, 495 | { 496 | "cell_type": "code", 497 | "execution_count": 17, 498 | "metadata": {}, 499 | "outputs": [ 500 | { 501 | "name": "stdout", 502 | "output_type": "stream", 503 | "text": [ 504 | "Our minimum x value for our random search (ridiculous test) is: 7.572346332547564 based on a y min of -22.175349266642858\n" 505 | ] 506 | } 507 | ], 508 | "source": [ 509 | "random_search_x_ridiculous = [random.uniform(-12.0, 12.0) for x in range(1000000)]\n", 510 | "random_search_test_ridiculous = (lambda x: objective_function(x))(random_search_x_ridiculous)\n", 511 | "print(\"Our minimum x value for our random search (ridiculous test) is: {} based on a y min of {}\".format(\n", 512 | " random_search_x_ridiculous[np.argmin(random_search_test_ridiculous)], random_search_test_ridiculous.min()))" 513 | ] 514 | }, 515 | { 516 | "cell_type": "markdown", 517 | "metadata": {}, 518 | "source": [ 519 | "Now that... that is impressive. We're VERY close to the actual confirmed global minima for this function.
\n", 520 | "\n", 521 | "Let's see how hyperopt does, though..." 522 | ] 523 | }, 524 | { 525 | "cell_type": "markdown", 526 | "metadata": {}, 527 | "source": [ 528 | "### Listing 7.2 Hyperopt search space for a one-dimensional polynomial optimization pattern\n", 529 | "This is the code listing from the book for this section (minus the import statement for hyperopt modules which is at the top of this notebook)." 530 | ] 531 | }, 532 | { 533 | "cell_type": "code", 534 | "execution_count": 18, 535 | "metadata": {}, 536 | "outputs": [ 537 | { 538 | "name": "stdout", 539 | "output_type": "stream", 540 | "text": [ 541 | "100%|██████████| 1000/1000 [00:09<00:00, 103.58trial/s, best loss: -22.175349000150717]\n" 542 | ] 543 | } 544 | ], 545 | "source": [ 546 | "# To make it fair, we're going to use a uniform search space as well.\n", 547 | "optimization_space = hp.uniform('x', -12, 12)\n", 548 | "\n", 549 | "trials = Trials()\n", 550 | "\n", 551 | "trial_estimator = fmin(fn=objective_function, \n", 552 | " space=optimization_space, \n", 553 | " algo=tpe.suggest,\n", 554 | " trials=trials,\n", 555 | " max_evals=1000 \n", 556 | " ) " 557 | ] 558 | }, 559 | { 560 | "cell_type": "markdown", 561 | "metadata": {}, 562 | "source": [ 563 | "### Listing 7.3 Demonstration of hyperopt performance in minimizing a simple polynomial function\n", 564 | "And here is where we are going to let hyperopt find the minima as best it can..." 565 | ] 566 | }, 567 | { 568 | "cell_type": "code", 569 | "execution_count": 19, 570 | "metadata": {}, 571 | "outputs": [ 572 | { 573 | "data": { 574 | "image/png": "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\n", 575 | "text/plain": [ 576 | "
" 577 | ] 578 | }, 579 | "metadata": {}, 580 | "output_type": "display_data" 581 | } 582 | ], 583 | "source": [ 584 | "rng = np.arange(-12.0, 12.0, 0.01).tolist()\n", 585 | "values = (lambda x: objective_function(x))(rng)\n", 586 | "with plt.style.context(style='seaborn'):\n", 587 | " fig, ax = plt.subplots(1, 1, figsize=(5.5, 4))\n", 588 | " ax.plot(rng, values)\n", 589 | " ax.set_title('Objective function')\n", 590 | " ax.scatter(x=trial_estimator['x'], y=trials.average_best_error(), marker='o', s=100)\n", 591 | " bbox_text = 'Hyperopt calculated minimum value\\nx: {}'.format(trial_estimator['x'])\n", 592 | " arrow = dict(facecolor='darkblue', shrink=0.01, connectionstyle='angle3,angleA=90,angleB=45')\n", 593 | " bbox_conf = dict(boxstyle='round,pad=0.5', fc='ivory', ec='grey', lw=0.8)\n", 594 | " conf = dict(xycoords='data', textcoords='axes fraction', arrowprops=arrow, bbox=bbox_conf, ha='left', va='center', fontsize=12)\n", 595 | " ax.annotate(bbox_text, xy=(trial_estimator['x'], trials.average_best_error()), xytext=(0.3, 0.8), **conf)\n", 596 | " fig.tight_layout()\n", 597 | " plt.savefig('objective_func.png', format='png', dpi=1000)" 598 | ] 599 | }, 600 | { 601 | "cell_type": "code", 602 | "execution_count": 20, 603 | "metadata": {}, 604 | "outputs": [ 605 | { 606 | "name": "stdout", 607 | "output_type": "stream", 608 | "text": [ 609 | "Grid Search absolute error: 0.06999999999958106\n", 610 | "Random Search absolute error: 0.01073155580355234\n", 611 | "Random Search Ridiculous absolute error: 0.00234633254798311\n", 612 | "Hyperopt absolute error: 0.002698416416993865\n" 613 | ] 614 | } 615 | ], 616 | "source": [ 617 | "ground_truth = x_axis_values[np.argmin(y_values)]\n", 618 | "grid_search_error = np.abs(our_grid[np.argmin(grid_search_test)] - ground_truth)\n", 619 | "random_search_error = np.abs(random_search_x[np.argmin(random_search_test)] - ground_truth)\n", 620 | "random_search_ridiculous_error = np.abs(\n", 621 | " random_search_x_ridiculous[np.argmin(random_search_test_ridiculous)] - ground_truth)\n", 622 | "hyperopt_error = np.abs(trial_estimator['x'] - ground_truth)\n", 623 | "\n", 624 | "print(\"Grid Search absolute error: {}\".format(grid_search_error))\n", 625 | "print(\"Random Search absolute error: {}\".format(random_search_error))\n", 626 | "print(\"Random Search Ridiculous absolute error: {}\".format(random_search_ridiculous_error))\n", 627 | "print(\"Hyperopt absolute error: {}\".format(hyperopt_error))" 628 | ] 629 | }, 630 | { 631 | "cell_type": "markdown", 632 | "metadata": {}, 633 | "source": [ 634 | "I think we can safely agree that Grid search may not be the most optimal way to tune models. It's slow, inefficient, and requires many runs to 'fine tune' the search space to get something approximating a 'well tuned model'.
\n", 635 | "The interesting results here are between hyperopt and random search.

\n", 636 | "While the 'Ridiculous (1 million iterations)' Random Search optimization beat hyperopt, it did so by a relatively small margin. However, it did so through training 999,000 more calculations than hyperopt did. Doesn't really seem like it would scale to your real-world supervised learning problem, does it?
\n", 637 | "If we compare hyperopt to the results from the iterations-matched random search with the same exact search space using the same sampling distribution (uniform), hyperopt beat random search soundly.
\n", 638 | "> This is why hyperopt was invented and why it's so very powerful. It gives fantastic results in far fewer iterations than other implementations that are widely used in the ML community.\n", 639 | "\n", 640 | ">> NOTE: Since the Random Search and HyperOpt both are using sampling algorithms that rely on random float selection, even though from the same spaces, these algorithms are non-deterministic. Each time that you run them you will get different results from the optimization. However, with enough iterations over these runs, you will find that the general performance will hold true across these different implementations for finding the global minima of this function." 641 | ] 642 | } 643 | ], 644 | "metadata": { 645 | "kernelspec": { 646 | "display_name": "Python 3", 647 | "language": "python", 648 | "name": "python3" 649 | }, 650 | "language_info": { 651 | "codemirror_mode": { 652 | "name": "ipython", 653 | "version": 3 654 | }, 655 | "file_extension": ".py", 656 | "mimetype": "text/x-python", 657 | "name": "python", 658 | "nbconvert_exporter": "python", 659 | "pygments_lexer": "ipython3", 660 | "version": "3.8.3" 661 | } 662 | }, 663 | "nbformat": 4, 664 | "nbformat_minor": 4 665 | } 666 | -------------------------------------------------------------------------------- /notebooks/ch08/Chapter8_1.dbc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenWilson2/ML-Engineering/0fc05f4b876b26bbacc85bcb11c7c2aef517cd20/notebooks/ch08/Chapter8_1.dbc -------------------------------------------------------------------------------- /notebooks/ch09/CleanCode.py: -------------------------------------------------------------------------------- 1 | import warnings as warn 2 | import pandas as pd 3 | import numpy as np 4 | import scipy 5 | import scipy.stats as stat 6 | from scipy.stats import shapiro, normaltest, anderson 7 | import matplotlib.pyplot as plt 8 | from statsmodels.graphics.gofplots import qqplot 9 | 10 | 11 | class DistributionAnalysis(object): 12 | 13 | def __init__(self, series, histogram_bins, **kwargs): 14 | self.series = series 15 | self.histogram_bins = histogram_bins 16 | self.series_name = kwargs.get('series_name', 'data') 17 | self.plot_bins = kwargs.get('plot_bins', 200) 18 | self.best_plot_size = kwargs.get('best_plot_size', (20, 16)) 19 | self.all_plot_size = kwargs.get('all_plot_size', (24, 30)) 20 | self.MIN_BOUNDARY = 0.001 21 | self.MAX_BOUNDARY = 0.999 22 | self.ALPHA = kwargs.get('alpha', 0.05) 23 | 24 | def _get_series_bins(self): 25 | return int(np.ceil(self.series.index.values.max())) 26 | 27 | @staticmethod 28 | def _get_distributions(): 29 | scipy_ver = scipy.__version__ 30 | if (int(scipy_ver[2]) >= 5) and (int(scipy_ver[4:]) > 3): 31 | names, gen_names = stat.get_distribution_names(stat.pairs, stat.rv_continuous) 32 | else: 33 | names = stat._continuous_distns._distn_names 34 | return names 35 | 36 | @staticmethod 37 | def _extract_params(params): 38 | return {'arguments': params[:-2], 'location': params[-2], 'scale': params[-1]} 39 | 40 | @staticmethod 41 | def _generate_boundaries(distribution, parameters, x): 42 | args = parameters['arguments'] 43 | loc = parameters['location'] 44 | scale = parameters['scale'] 45 | return distribution.ppf(x, *args, loc=loc, scale=scale) if args else distribution.ppf(x, loc=loc, scale=scale) 46 | 47 | @staticmethod 48 | def _build_pdf(x, distribution, parameters): 49 | if parameters['arguments']: 50 | pdf = distribution.pdf(x, loc=parameters['location'], scale=parameters['scale'], *parameters['arguments']) 51 | else: 52 | pdf = distribution.pdf(x, loc=parameters['location'], scale=parameters['scale']) 53 | return pdf 54 | 55 | def plot_normalcy(self): 56 | qqplot(self.series, line='s') 57 | 58 | def check_normalcy(self): 59 | 60 | def significance_test(value, threshold): 61 | return "Data set {} normally distributed from".format('is' if value > threshold else 'is not') 62 | 63 | shapiro_stat, shapiro_p_value = shapiro(self.series) 64 | dagostino_stat, dagostino_p_value = normaltest(self.series) 65 | anderson_stat, anderson_crit_vals, anderson_significance_levels = anderson(self.series) 66 | anderson_report = list(zip(list(anderson_crit_vals), list(anderson_significance_levels))) 67 | shapiro_statement = """Shapiro-Wilk stat: {:.4f} 68 | Shapiro-Wilk test p-Value: {:.4f} 69 | {} Shapiro-Wilk Test""".format( 70 | shapiro_stat, shapiro_p_value, significance_test(shapiro_p_value, self.ALPHA)) 71 | 72 | dagostino_statement = """\nD'Agostino stat: {:.4f} 73 | D'Agostino test p-Value: {:.4f} 74 | {} D'Agostino Test""".format( 75 | dagostino_stat, dagostino_p_value, significance_test(dagostino_p_value, self.ALPHA)) 76 | 77 | anderson_statement = '\nAnderson statistic: {:.4f}'.format(anderson_stat) 78 | for i in anderson_report: 79 | anderson_statement = anderson_statement + """ 80 | For signifance level {} of Anderson-Darling test: {} the evaluation. Critical value: {}""".format( 81 | i[1], significance_test(i[0], anderson_stat), i[0]) 82 | 83 | return "{}{}{}".format(shapiro_statement, dagostino_statement, anderson_statement) 84 | 85 | def _calculate_fit_loss(self, x, y, dist): 86 | with warn.catch_warnings(): 87 | warn.filterwarnings('ignore') 88 | estimated_distribution = dist.fit(x) 89 | params = self._extract_params(estimated_distribution) 90 | pdf = self._build_pdf(x, dist, params) 91 | return np.sum(np.power(y - pdf, 2.0)), estimated_distribution 92 | 93 | def _generate_probability_distribution(self, distribution, parameters, bins): 94 | starting_point = self._generate_boundaries(distribution, parameters, self.MIN_BOUNDARY) 95 | ending_point = self._generate_boundaries(distribution, parameters, self.MAX_BOUNDARY) 96 | x = np.linspace(starting_point, ending_point, bins) 97 | y = self._build_pdf(x, distribution, parameters) 98 | return pd.Series(y, x) 99 | 100 | def find_distribution_fit(self): 101 | 102 | y_hist, x_hist_raw = np.histogram(self.series, self.histogram_bins, density=True) 103 | x_hist = (x_hist_raw + np.roll(x_hist_raw, -1))[:-1] / 2. 104 | full_distribution_results = {} 105 | 106 | best_loss = np.inf 107 | best_fit = stat.norm 108 | best_params = (0., 1.) 109 | for dist in self._get_distributions(): 110 | histogram = getattr(stat, dist) 111 | results, parameters = self._calculate_fit_loss(x_hist, y_hist, histogram) 112 | full_distribution_results[dist] = {'hist': histogram, 113 | 'loss': results, 114 | 'params': { 115 | 'arguments': parameters[:-2], 116 | 'location': parameters[-2], 117 | 'scale': parameters[-1] 118 | } 119 | } 120 | if best_loss > results > 0: 121 | best_loss = results 122 | best_fit = histogram 123 | best_params = parameters 124 | return {'best_distribution': best_fit, 125 | 'best_loss': best_loss, 126 | 'best_params': { 127 | 'arguments': best_params[:-2], 128 | 'location': best_params[-2], 129 | 'scale': best_params[-1] 130 | }, 131 | 'all_results': full_distribution_results 132 | } 133 | 134 | def plot_best_fit(self): 135 | 136 | fits = self.find_distribution_fit() 137 | best_fit_distribution = fits['best_distribution'] 138 | best_fit_parameters = fits['best_params'] 139 | distribution_series = self._generate_probability_distribution(best_fit_distribution, 140 | best_fit_parameters, 141 | self._get_series_bins()) 142 | with plt.style.context(style='seaborn'): 143 | fig = plt.figure(figsize=self.best_plot_size) 144 | ax = self.series.plot(kind='hist', bins=self.plot_bins, normed=True, 145 | alpha=0.5, label=self.series_name, legend=True) 146 | distribution_series.plot(lw=3, label=best_fit_distribution.__class__.__name__, legend=True, ax=ax) 147 | ax.legend(loc='best') 148 | return fig 149 | 150 | def plot_all_fits(self): 151 | 152 | fits = self.find_distribution_fit() 153 | series_bins = self._get_series_bins() 154 | 155 | with warn.catch_warnings(): 156 | warn.filterwarnings('ignore') 157 | with plt.style.context(style='seaborn'): 158 | fig = plt.figure(figsize=self.all_plot_size) 159 | ax = self.series.plot(kind='hist', bins=self.plot_bins, normed=True, alpha=0.5, 160 | label=self.series_name, legend=True) 161 | y_max = ax.get_ylim() 162 | x_max = ax.get_xlim() 163 | for dist in fits['all_results']: 164 | hist = fits['all_results'][dist] 165 | distribution_data = self._generate_probability_distribution(hist['hist'], 166 | hist['params'], 167 | series_bins) 168 | distribution_data.plot(lw=2, label=dist, alpha=0.6, ax=ax) 169 | ax.legend(loc='best') 170 | ax.set_ylim(y_max) 171 | ax.set_xlim(x_max) 172 | return fig 173 | -------------------------------------------------------------------------------- /notebooks/ch09/UnitTestExample.py: -------------------------------------------------------------------------------- 1 | from .CleanCode import DistributionAnalysis 2 | import numpy as np 3 | import scipy.stats as stat 4 | 5 | 6 | def test_generate_boundaries(): 7 | expected_low_norm = -2.3263478740408408 8 | expected_high_norm = 2.3263478740408408 9 | boundary_arguments = {'location': 0, 'scale': 1, 'arguments': ()} 10 | test_object = DistributionAnalysis(np.arange(0, 100), 10) 11 | normal_distribution_low = test_object._generate_boundaries(stat.norm, 12 | boundary_arguments, 13 | 0.01) 14 | normal_distribution_high = test_object._generate_boundaries(stat.norm, 15 | boundary_arguments, 16 | 0.99) 17 | assert normal_distribution_low == expected_low_norm, \ 18 | 'Normal Dist low boundary: {} does not match expected: {}' \ 19 | .format(normal_distribution_low, expected_low_norm) 20 | assert normal_distribution_high == expected_high_norm, \ 21 | 'Normal Dist high boundary: {} does not match expected: {}' \ 22 | .format(normal_distribution_high, expected_high_norm) 23 | 24 | 25 | if __name__ == '__main__': 26 | test_generate_boundaries() 27 | print('tests passed') 28 | -------------------------------------------------------------------------------- /notebooks/ch09/WoT.py: -------------------------------------------------------------------------------- 1 | import warnings as warn 2 | import pandas as pd 3 | import numpy as np 4 | import scipy.stats as stat 5 | from scipy.stats import shapiro, normaltest, anderson 6 | import matplotlib.pyplot as plt 7 | from statsmodels.graphics.gofplots import qqplot 8 | 9 | ### NOTE: This is for demonstration purposes ONLY. DO NOT run this 10 | ### if you want to use your computer for the next few hours. 11 | 12 | data = pd.read_csv('/sf-airbnb-clean.csv') 13 | series = data['price'] 14 | shapiro, pval = shapiro(series) 15 | print('Shapiro score: ' + str(shapiro) + ' with pvalue: ' + str(pval)) 16 | dagastino, pval = normaltest(series) 17 | print("D'Agostino score: " + str(dagastino) + " with pvalue: " + str(pval)) 18 | anderson_stat, crit, sig = anderson(series) 19 | print("Anderson statistic: " + str(anderson_stat)) 20 | anderson_rep = list(zip(list(crit), list(sig))) 21 | for i in anderson_rep: 22 | print('Significance: ' + str(i[0]) + ' Crit level: ' + str(i[1])) 23 | bins = int(np.ceil(series.index.values.max())) 24 | y, x = np.histogram(series, 200, density=True) 25 | x = (x + np.roll(x, -1))[:-1] / 2. 26 | bl = np.inf 27 | bf = stat.norm 28 | bp = (0., 1.) 29 | with warn.catch_warnings(): 30 | warn.filterwarnings('ignore') 31 | fam = stat._continuous_distns._distn_names 32 | for d in fam: 33 | h = getattr(stat, d) 34 | f = h.fit(series) 35 | pdf = h.pdf(x, loc=f[-2], scale=f[-1], *f[:-2]) 36 | loss = np.sum(np.power(y - pdf, 2.)) 37 | if bl > loss > 0: 38 | bl = loss 39 | bf = h 40 | bp = f 41 | start = bf.ppf(0.001, *bp[:-2], loc=bp[-2], scale=bp[-1]) 42 | end = bf.ppf(0.999, *bp[:-2], loc=bp[-2], scale=bp[-1]) 43 | xd = np.linspace(start, end, bins) 44 | yd = bf.pdf(xd, loc=bp[-2], scale=bp[-1], *bp[:-2]) 45 | hdist = pd.Series(yd, xd) 46 | with warn.catch_warnings(): 47 | warn.filterwarnings('ignore') 48 | with plt.style.context(style='seaborn'): 49 | fig = plt.figure(figsize=(16,12)) 50 | ax = series.plot(kind='hist', bins=100, normed=True, alpha=0.5, label='Airbnb SF Price', legend=True) 51 | ymax = ax.get_ylim() 52 | xmax = ax.get_xlim() 53 | hdist.plot(lw=3, label='best dist ' + bf.__class__.__name__, legend=True, ax=ax) 54 | ax.legend(loc='best') 55 | ax.set_xlim(xmax) 56 | ax.set_ylim(ymax) 57 | qqplot(series, line='s') 58 | -------------------------------------------------------------------------------- /notebooks/ch09/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenWilson2/ML-Engineering/0fc05f4b876b26bbacc85bcb11c7c2aef517cd20/notebooks/ch09/__init__.py -------------------------------------------------------------------------------- /notebooks/ch13/Chapter_13.dbc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenWilson2/ML-Engineering/0fc05f4b876b26bbacc85bcb11c7c2aef517cd20/notebooks/ch13/Chapter_13.dbc -------------------------------------------------------------------------------- /notebooks/ch13/Chapter_13.scala: -------------------------------------------------------------------------------- 1 | // Databricks notebook source 2 | // MAGIC %md 3 | // MAGIC # Chapter 13 ML Engineering in Action 4 | // MAGIC Author: Ben Wilson 5 | 6 | // COMMAND ---------- 7 | 8 | // MAGIC %md 9 | // MAGIC ## ML Development Hubris 10 | // MAGIC In this notebook, we'll be looking at some patterns of development that can cause problems in projects. Specifically, issues that arise from unintentionally obfuscated (overly complex) code, prematurely / incorrectly optimized code, and the complexities introduced from early generalization of a code base. 11 | 12 | // COMMAND ---------- 13 | 14 | // MAGIC %md Before we get into the examples that are in the book, we need to generate some data for the use cases here. Keeping with the common theme of this part of the book, we're talking about dogs again, as well as the one thing that they care about more than anything: food. 15 | 16 | // COMMAND ---------- 17 | 18 | import org.apache.spark.ml.Pipeline 19 | import org.apache.spark.ml.classification.{DecisionTreeClassifier, DecisionTreeClassificationModel} 20 | import org.apache.spark.ml.feature.{StringIndexer, StringIndexerModel, VectorAssembler, IndexToString} 21 | import org.apache.spark.ml.PipelineModel 22 | import org.apache.spark.ml.evaluation.{ 23 | BinaryClassificationEvaluator, 24 | MulticlassClassificationEvaluator} 25 | import org.apache.spark.sql.DataFrame 26 | import org.apache.spark.sql.types._ 27 | import org.apache.spark.sql.functions._ 28 | import scala.util.Random 29 | import scala.reflect.ClassTag 30 | import scala.collection.mutable.ArrayBuffer 31 | 32 | // COMMAND ---------- 33 | 34 | case class Dogs(age: Int, 35 | weight: Double, 36 | favorite_food: String, 37 | breed: String, 38 | good_boy_or_girl: String, 39 | hungry: Boolean) 40 | 41 | case object CoreData { 42 | def dogBreeds: Seq[String] = 43 | Seq( 44 | "Husky", 45 | "GermanShepherd", 46 | "Dalmation", 47 | "Pug", 48 | "Malamute", 49 | "Akita", 50 | "BelgianMalinois", 51 | "Chinook", 52 | "Estrela", 53 | "Doberman", 54 | "Mastiff" 55 | ) 56 | def foods: Seq[String] = 57 | Seq( 58 | "Kibble", 59 | "Spaghetti", 60 | "Labneh", 61 | "Steak", 62 | "Hummus", 63 | "Fajitas", 64 | "BeoufBourgignon", 65 | "Bolognese" 66 | ) 67 | def goodness: Seq[String] = 68 | Seq("yes", "no", "sometimes", "yesWhenFoodAvailable") 69 | def hungry: Seq[Boolean] = Seq(true, false) 70 | def ageSigma = 3 71 | def ageMean = 2 72 | def weightSigma = 12 73 | def weightMean = 60 74 | } 75 | 76 | trait DogUtility { 77 | 78 | def getDoggoData[T: ClassTag](a: Seq[T], dogs: Int, seed: Long): Seq[T] = { 79 | val rnd = new Random(seed) 80 | Seq.fill(dogs)(a(rnd.nextInt(a.size))) 81 | } 82 | 83 | def getDistributedIntData(sigma: Double, 84 | mean: Double, 85 | dogs: Int, 86 | seed: Long): Seq[Int] = { 87 | val rnd = new Random(seed) 88 | (0 until dogs).map( 89 | _ => math.ceil(math.abs(rnd.nextGaussian() * sigma + mean)).toInt 90 | ) 91 | } 92 | 93 | def getDistributedDoubleData(sigma: Double, 94 | mean: Double, 95 | dogs: Int, 96 | seed: Long): Seq[Double] = { 97 | val rnd = new Random(seed) 98 | (0 until dogs).map( 99 | _ => 100 | math 101 | .round(math.abs(rnd.nextGaussian() * sigma * 100 + mean)) 102 | .toDouble / 100 103 | ) 104 | } 105 | 106 | } 107 | 108 | object DogDataGeneration extends DogUtility { 109 | 110 | def generateData(rows: Int, seed: Long): DataFrame = { 111 | 112 | val ageData = getDistributedIntData(CoreData.ageSigma, CoreData.ageMean, rows, seed) 113 | val weightData = getDistributedDoubleData(CoreData.weightSigma, CoreData.weightMean, rows, seed) 114 | val foodData = getDoggoData(CoreData.foods, rows, seed) 115 | val breedData = getDoggoData(CoreData.dogBreeds, rows, seed) 116 | val goodData = getDoggoData(CoreData.goodness, rows, seed) 117 | val hungryData = getDoggoData(CoreData.hungry, rows, seed) 118 | val collection = (0 until rows).toArray.map(x => { 119 | Dogs(ageData(x), weightData(x), foodData(x), breedData(x), goodData(x), hungryData(x)) 120 | }).toSeq 121 | collection.toDF() 122 | } 123 | 124 | def generateColl(rows: Int, seed: Long) = { 125 | 126 | val ageData = getDistributedIntData(CoreData.ageSigma, CoreData.ageMean, rows, seed) 127 | val weightData = getDistributedDoubleData(CoreData.weightSigma, CoreData.weightMean, rows, seed) 128 | val foodData = getDoggoData(CoreData.foods, rows, seed) 129 | val breedData = getDoggoData(CoreData.dogBreeds, rows, seed) 130 | val goodData = getDoggoData(CoreData.goodness, rows, seed) 131 | val hungryData = getDoggoData(CoreData.hungry, rows, seed) 132 | val collection = Seq(ageData, weightData, foodData, breedData, goodData, hungryData) 133 | collection 134 | } 135 | 136 | } 137 | 138 | // COMMAND ---------- 139 | 140 | // MAGIC %md ```Note: the object DogDataGeneration is not a very optimal way of generating data. We will be covering a more optimal methodology in listing 13.10``` 141 | 142 | // COMMAND ---------- 143 | 144 | // MAGIC %md To get the initial data set for listing 13.1, let's use the sub-optimal generator above to build it. 145 | 146 | // COMMAND ---------- 147 | 148 | val dataLarger = DogDataGeneration.generateData(100000, 42L) 149 | .withColumn("hungry", when(col("hungry"), "true").otherwise("false")) 150 | .withColumn("hungry", when(col("breed") === "Husky", "true").otherwise(col("hungry"))) 151 | .withColumn("good_boy_or_girl", when(col("breed") === "Husky", "yesWhenFoodAvailable") 152 | .otherwise(col("good_boy_or_girl"))) 153 | 154 | // COMMAND ---------- 155 | 156 | // MAGIC %md 157 | // MAGIC ###Listing 13.1 Imperative model prototype 158 | // MAGIC In this code listing, we see an example of building a fairly standard SparkML DecisionTreeClassifier model. The code style, highly imperative in nature, is indicative of how a lot of demos are written, reference implementations, and how a large amount of experimentation work is conducted. 159 | 160 | // COMMAND ---------- 161 | 162 | val DATA_SOURCE = dataLarger 163 | 164 | val indexerFood = new StringIndexer() 165 | .setInputCol("favorite_food") 166 | .setOutputCol("favorite_food_si") 167 | .setHandleInvalid("keep") 168 | .fit(DATA_SOURCE) 169 | 170 | val indexerBreed = new StringIndexer() 171 | .setInputCol("breed") 172 | .setOutputCol("breed_si") 173 | .setHandleInvalid("keep") 174 | .fit(DATA_SOURCE) 175 | 176 | val indexerGood = new StringIndexer() 177 | .setInputCol("good_boy_or_girl") 178 | .setOutputCol("good_boy_or_girl_si") 179 | .setHandleInvalid("keep") 180 | .fit(DATA_SOURCE) 181 | 182 | val indexerHungry = new StringIndexer() 183 | .setInputCol("hungry") 184 | .setOutputCol("hungry_si") 185 | .setHandleInvalid("error") 186 | .fit(DATA_SOURCE) 187 | 188 | val Array(train, test) = DATA_SOURCE.randomSplit(Array(0.75, 0.25)) 189 | 190 | val indexerLabelConversion = new IndexToString() 191 | .setInputCol("prediction") 192 | .setOutputCol("predictionLabel") 193 | .setLabels(indexerHungry.labelsArray(0)) 194 | 195 | val assembler = new VectorAssembler() 196 | .setInputCols(Array("age", "weight", "favorite_food_si", "breed_si", "good_boy_or_girl_si")) 197 | .setOutputCol("features") 198 | 199 | val decisionTreeModel = new DecisionTreeClassifier() 200 | .setLabelCol("hungry_si") 201 | .setFeaturesCol("features") 202 | .setImpurity("gini") 203 | .setMinInfoGain(1e-4) 204 | .setMaxDepth(6) 205 | .setMinInstancesPerNode(5) 206 | .setMinWeightFractionPerNode(0.05) 207 | 208 | val pipeline = new Pipeline() 209 | .setStages(Array(indexerFood, indexerBreed, indexerGood, indexerHungry, assembler, decisionTreeModel, indexerLabelConversion)) 210 | 211 | val model = pipeline.fit(train) 212 | 213 | val predictions = model.transform(test) 214 | 215 | val lossMetric = new BinaryClassificationEvaluator() 216 | .setLabelCol("hungry_si") 217 | .setRawPredictionCol("prediction") 218 | .setMetricName("areaUnderROC") 219 | .evaluate(predictions) 220 | 221 | 222 | 223 | // COMMAND ---------- 224 | 225 | // MAGIC %md ### Listing 13.2 Overly complex model prototype 226 | // MAGIC What happens if someone who is trying to 'get fancy' tries their hand at building a prototype? What if they just want to show off their coding skills (flexing) to the rest of the team? Just how convoluted could this potentially become? 227 | 228 | // COMMAND ---------- 229 | 230 | case class ModelReturn( 231 | pipeline: PipelineModel, 232 | metric: Double 233 | ) 234 | 235 | class BuildDecisionTree(data: DataFrame, 236 | trainPercent: Double, 237 | labelCol: String) { 238 | 239 | final val LABEL_COL = "label" 240 | final val FEATURES_COL = "features" 241 | final val PREDICTION_COL = "prediction" 242 | final val SCORING_METRIC = "areaUnderROC" 243 | 244 | private def constructIndexers(): Array[StringIndexerModel] = { 245 | data.schema 246 | .collect { 247 | case x if (x.dataType == StringType) & (x.name != labelCol) => x.name 248 | } 249 | .map { x => 250 | new StringIndexer() 251 | .setInputCol(x) 252 | .setOutputCol(s"${x}_si") 253 | .setHandleInvalid("keep") 254 | .fit(data) 255 | } 256 | .toArray 257 | 258 | } 259 | 260 | private def indexLabel(): StringIndexerModel = { 261 | data.schema.collect { 262 | case x if (x.name == labelCol) & (x.dataType == StringType) => 263 | new StringIndexer() 264 | .setInputCol(x.name) 265 | .setOutputCol(LABEL_COL) 266 | .setHandleInvalid("error") 267 | .fit(data) 268 | }.head 269 | } 270 | 271 | private def labelInversion( 272 | labelIndexer: StringIndexerModel 273 | ): IndexToString = { 274 | new IndexToString() 275 | .setInputCol(PREDICTION_COL) 276 | .setOutputCol(s"${LABEL_COL}_${PREDICTION_COL}") 277 | .setLabels(labelIndexer.labelsArray(0)) 278 | } 279 | 280 | private def buildVector( 281 | featureIndexers: Array[StringIndexerModel] 282 | ): VectorAssembler = { 283 | 284 | val featureSchema = data.schema.names.filterNot(_.contains(labelCol)) 285 | val updatedSchema = featureIndexers.map(_.getInputCol) 286 | val features = featureSchema.filterNot(updatedSchema.contains) ++ featureIndexers 287 | .map(_.getOutputCol) 288 | new VectorAssembler() 289 | .setInputCols(features) 290 | .setOutputCol(FEATURES_COL) 291 | } 292 | 293 | private def buildDecisionTree(): DecisionTreeClassifier = { 294 | new DecisionTreeClassifier() 295 | .setLabelCol(LABEL_COL) 296 | .setFeaturesCol(FEATURES_COL) 297 | .setImpurity("entropy") 298 | .setMinInfoGain(1e-7) 299 | .setMaxDepth(6) 300 | .setMinInstancesPerNode(5) 301 | } 302 | 303 | private def scorePipeline(testData: DataFrame, pipeline: PipelineModel): Double = { 304 | new BinaryClassificationEvaluator() 305 | .setLabelCol(LABEL_COL) 306 | .setRawPredictionCol(PREDICTION_COL) 307 | .setMetricName(SCORING_METRIC) 308 | .evaluate(pipeline.transform(testData)) 309 | } 310 | 311 | def buildPipeline(): ModelReturn = { 312 | 313 | val featureIndexers = constructIndexers() 314 | val labelIndexer = indexLabel() 315 | val vectorAssembler = buildVector(featureIndexers) 316 | val Array(train, test) = data.randomSplit(Array(trainPercent, 1.0-trainPercent)) 317 | val pipeline = new Pipeline() 318 | .setStages( 319 | featureIndexers ++ 320 | Array( 321 | labelIndexer, 322 | vectorAssembler, 323 | buildDecisionTree(), 324 | labelInversion(labelIndexer) 325 | ) 326 | ) 327 | .fit(train) 328 | 329 | ModelReturn(pipeline, scorePipeline(test, pipeline)) 330 | 331 | } 332 | 333 | } 334 | 335 | object BuildDecisionTree { 336 | def apply(data: DataFrame, 337 | trainPercent: Double, 338 | labelCol: String): BuildDecisionTree = 339 | new BuildDecisionTree(data, trainPercent, labelCol) 340 | } 341 | 342 | // COMMAND ---------- 343 | 344 | val build = BuildDecisionTree(DATA_SOURCE, 0.75, "hungry").buildPipeline() 345 | display(build.pipeline.transform(dataLarger)) 346 | 347 | // COMMAND ---------- 348 | 349 | // MAGIC %md I don't know about you, but I wouldn't want to have to modify this code during the process of testing different features, adapting to model architecture changes, and adding in additional feature engineering steps. It's complicated, tightly-coupled, and looks more like generic framework code than job code.
350 | // MAGIC Were this a final-stage build of the code after the features and all steps involving the modeling completed, this code could potentially make some sense (and even then, it's very difficult to test) from a final production version standpoint. However, in an early phase (as this is clearly at with respect to the level of customization involved), this introduces more chaos than it prevents. It's just far too over-engineered to be useful. 351 | 352 | // COMMAND ---------- 353 | 354 | // MAGIC %md As a bonus (this isn't covered in the text of the book), here's an example of a truly over-engineered analysis of the extracted information from a decision tree model. As you peruse this code, try to imagine having to update a part of it during a development cycle to support additional functionality.
355 | // MAGIC Code like this belongs firmly embedded within a utility framework and should be fully external to any project work. It's a useful bit of code for analyzing the results of a tree-based algorithm, but it's far too complex and singularly focused / generic to be put into the solution repo for a project. 356 | 357 | // COMMAND ---------- 358 | 359 | import org.apache.spark.ml.PipelineModel 360 | import org.apache.spark.ml.tree.{ContinuousSplit, InternalNode, Node, Split} 361 | import org.apache.spark.ml.classification.{ 362 | DecisionTreeClassificationModel, 363 | RandomForestClassificationModel 364 | } 365 | import org.apache.spark.ml.tree.{ 366 | CategoricalSplit, 367 | ContinuousSplit, 368 | InternalNode, 369 | Node 370 | } 371 | import org.json4s.jackson.Serialization 372 | import org.json4s.jackson.Serialization.writePretty 373 | import org.json4s.{Formats, FullTypeHints} 374 | 375 | case class NodeData( 376 | featureIndex: Option[Int], 377 | informationGain: Option[Double], 378 | continuousSplitThreshold: Option[Double], 379 | treeNodeType: String, 380 | splitType: Option[String], 381 | leftNodeCategories: Option[Array[Double]], 382 | rightNodeCategories: Option[Array[Double]], 383 | leftChild: Option[NodeData], 384 | rightChild: Option[NodeData], 385 | prediction: Double 386 | ) 387 | 388 | object PayloadType extends Enumeration { 389 | type PayloadType = Value 390 | val MODEL, PIPELINE = Value 391 | } 392 | 393 | object NodeType extends Enumeration { 394 | type NodeType = Value 395 | val NODE, LEAF = Value 396 | } 397 | 398 | object SplitType extends Enumeration { 399 | type SplitType = Value 400 | val CONTINUOUS, CATEGORICAL = Value 401 | } 402 | 403 | object PayloadDetermination { 404 | 405 | import PayloadType._ 406 | 407 | def payloadType[T](value: T): PayloadType = { 408 | value match { 409 | case _: PipelineModel => PIPELINE 410 | case _ => MODEL 411 | } 412 | } 413 | 414 | } 415 | 416 | case class TreesReport(tree: Int, data: NodeData) 417 | 418 | case class FeatureIndexRenamingStructure(featureName: String, 419 | replacementText: String) 420 | 421 | def refactorDebugTree(debugStatement: String, 422 | features: Array[String]): String = { 423 | 424 | val featureMapping = features.zipWithIndex.map( 425 | x => FeatureIndexRenamingStructure(x._1, s"""featureIndex" : ${x._2}""") 426 | ) 427 | 428 | featureMapping.foldLeft(debugStatement) { 429 | case (debugString, field) => 430 | debugString.replaceAll(field.replacementText, s"""feature Index" : "${field.featureName}"""") 431 | } 432 | 433 | } 434 | 435 | object NodeDetermination { 436 | 437 | import NodeType._ 438 | import SplitType._ 439 | 440 | def nodeType(node: Node): NodeType = node match { 441 | case _: InternalNode => NODE 442 | case _ => LEAF 443 | } 444 | 445 | def splitType(split: Split): SplitType = split match { 446 | case x: ContinuousSplit => CONTINUOUS 447 | case _ => CATEGORICAL 448 | } 449 | 450 | } 451 | 452 | 453 | class TreeModelExtractor[T] 454 | object TreeModelExtractor { 455 | implicit object DecisionTreeClassifierExtractor 456 | extends TreeModelExtractor[DecisionTreeClassificationModel] 457 | implicit object RandomForestClassificationExtractor 458 | extends TreeModelExtractor[RandomForestClassificationModel] 459 | } 460 | 461 | 462 | object ModelDecisionExtractor { 463 | 464 | import NodeType._ 465 | import SplitType._ 466 | 467 | private def getSplitRuleSet(treeNode: Node): NodeData = { 468 | 469 | val nodeType = NodeDetermination.nodeType(treeNode) 470 | val internalNodeData = nodeType match { 471 | case NODE => Some(treeNode.asInstanceOf[InternalNode]) 472 | case LEAF => None 473 | } 474 | 475 | val splitType = nodeType match { 476 | case NODE => Some(NodeDetermination.splitType(internalNodeData.get.split)) 477 | case _ => None 478 | } 479 | 480 | NodeData( 481 | featureIndex = nodeType match { 482 | case NODE => Some(internalNodeData.get.split.featureIndex) 483 | case _ => None 484 | }, 485 | informationGain = nodeType match { 486 | case NODE => Some(internalNodeData.get.gain) 487 | case _ => None 488 | }, 489 | continuousSplitThreshold = splitType.getOrElse(None) match { 490 | case CONTINUOUS => 491 | Some( 492 | internalNodeData.get.split.asInstanceOf[ContinuousSplit].threshold 493 | ) 494 | case _ => None 495 | }, 496 | treeNodeType = nodeType match { 497 | case NODE => "node" 498 | case _ => "leaf" 499 | }, 500 | splitType = nodeType match { 501 | case NODE => 502 | splitType.get match { 503 | case CONTINUOUS => Some("continuous") 504 | case CATEGORICAL => Some("categorical") 505 | case _ => None 506 | } 507 | case _ => None 508 | }, 509 | leftNodeCategories = splitType.getOrElse(None) match { 510 | case CATEGORICAL => 511 | Some( 512 | internalNodeData.get.split 513 | .asInstanceOf[CategoricalSplit] 514 | .leftCategories 515 | ) 516 | case _ => None 517 | }, 518 | rightNodeCategories = splitType.getOrElse(None) match { 519 | case CATEGORICAL => 520 | Some( 521 | internalNodeData.get.split 522 | .asInstanceOf[CategoricalSplit] 523 | .rightCategories 524 | ) 525 | case _ => None 526 | }, 527 | leftChild = nodeType match { 528 | case NODE => Some(getSplitRuleSet(internalNodeData.get.leftChild)) 529 | case _ => None 530 | }, 531 | rightChild = nodeType match { 532 | case NODE => Some(getSplitRuleSet(internalNodeData.get.rightChild)) 533 | case _ => None 534 | }, 535 | prediction = treeNode.prediction 536 | ) 537 | 538 | } 539 | 540 | private def rulesToString(rules: NodeData): String = { 541 | implicit val jsonFormat: Formats = 542 | Serialization.formats(hints = FullTypeHints(List(NodeData.getClass))) 543 | writePretty(rules) 544 | } 545 | 546 | private def replaceFeatureIndicesWithText( 547 | rules: NodeData, 548 | featureVectorNames: Array[String] 549 | ): String = { 550 | 551 | featureVectorNames.zipWithIndex 552 | .map( 553 | x => 554 | FeatureIndexRenamingStructure(x._1, s""""featureIndex" : ${x._2}""") 555 | ) 556 | .foldLeft(rulesToString(rules)) { 557 | case (treeText, field) => 558 | treeText.replaceAll( 559 | field.replacementText, 560 | s"""""featureIndex" : "${field.featureName}"""" 561 | ) 562 | } 563 | } 564 | 565 | def extractModel[T: TreeModelExtractor]( 566 | model: T, 567 | featureVectorNames: Array[String] 568 | ): Array[String] = 569 | model match { 570 | case _: DecisionTreeClassificationModel => 571 | Array( 572 | replaceFeatureIndicesWithText( 573 | getSplitRuleSet( 574 | model.asInstanceOf[DecisionTreeClassificationModel].rootNode 575 | ), 576 | featureVectorNames 577 | ) 578 | ) 579 | case _: RandomForestClassificationModel => 580 | model 581 | .asInstanceOf[RandomForestClassificationModel] 582 | .trees 583 | .map( 584 | x => 585 | replaceFeatureIndicesWithText( 586 | getSplitRuleSet(x.rootNode), 587 | featureVectorNames 588 | ) 589 | ) 590 | } 591 | 592 | private def rulesToStringAlt(rules: TreesReport): String = { 593 | implicit val jsonFormat: Formats = 594 | Serialization.formats(hints = FullTypeHints(List(NodeData.getClass))) 595 | writePretty(rules) 596 | } 597 | 598 | private def replaceFeatureIndicesWithTextAlt( 599 | rules: TreesReport, 600 | featureVectorNames: Array[String] 601 | ): String = { 602 | 603 | featureVectorNames.zipWithIndex 604 | .map( 605 | x => 606 | FeatureIndexRenamingStructure(x._1, s""""featureIndex" : ${x._2}""") 607 | ) 608 | .foldLeft(rulesToStringAlt(rules)) { 609 | case (treeText, field) => 610 | treeText.replaceAll( 611 | field.replacementText, 612 | s""""featureIndex" : "${field.featureName}"""" 613 | ) 614 | } 615 | } 616 | 617 | def extractModelAlt[T: TreeModelExtractor]( 618 | model: T, 619 | featureVectorNames: Array[String] 620 | ): Array[String] = 621 | model match { 622 | case _: DecisionTreeClassificationModel => 623 | Array( 624 | getSplitRuleSet( 625 | model.asInstanceOf[DecisionTreeClassificationModel].rootNode 626 | ) 627 | ).zipWithIndex 628 | .map(x => TreesReport(x._2, x._1)) 629 | .map(replaceFeatureIndicesWithTextAlt(_, featureVectorNames)) 630 | case _: RandomForestClassificationModel => 631 | model 632 | .asInstanceOf[RandomForestClassificationModel] 633 | .trees 634 | .map(x => getSplitRuleSet(x.rootNode)) 635 | .zipWithIndex 636 | .map(x => TreesReport(x._2, x._1)) 637 | .map(replaceFeatureIndicesWithTextAlt(_, featureVectorNames)) 638 | } 639 | 640 | 641 | def extractTreesData[T: TreeModelExtractor]( 642 | model: T, 643 | featureVectorNames: Array[String] 644 | ): Array[TreesReport] = { 645 | 646 | model match { 647 | case _: DecisionTreeClassificationModel => 648 | Array( 649 | getSplitRuleSet( 650 | model.asInstanceOf[DecisionTreeClassificationModel].rootNode 651 | ) 652 | ).zipWithIndex.map(x => TreesReport(x._2, x._1)) 653 | case _: RandomForestClassificationModel => 654 | model 655 | .asInstanceOf[RandomForestClassificationModel] 656 | .trees 657 | .map(x => getSplitRuleSet(x.rootNode)) 658 | .zipWithIndex 659 | .map(x => TreesReport(x._2, x._1)) 660 | } 661 | 662 | } 663 | 664 | def blockConvertToJSON(rules: Array[TreesReport]): String = { 665 | implicit val jsonFormat: Formats = 666 | Serialization.formats(hints = FullTypeHints(List(TreesReport.getClass))) 667 | writePretty(rules) 668 | } 669 | 670 | def blockReplaceJSONText(rules: Array[TreesReport], 671 | featureVectorNames: Array[String]): String = { 672 | 673 | featureVectorNames.zipWithIndex 674 | .map( 675 | x => 676 | FeatureIndexRenamingStructure(x._1, s""""featureIndex" : ${x._2}""") 677 | ) 678 | .foldLeft(blockConvertToJSON(rules)) { 679 | case (treeText, field) => 680 | treeText.replaceAll( 681 | field.replacementText, 682 | s""""featureIndex" : "${field.featureName}"""" 683 | ) 684 | } 685 | } 686 | 687 | } 688 | 689 | // COMMAND ---------- 690 | 691 | // MAGIC %md And using the above monstrosity would look like this: 692 | 693 | // COMMAND ---------- 694 | 695 | val builtModel = build.pipeline.stages.takeRight(2).head.asInstanceOf[DecisionTreeClassificationModel] 696 | val features = build.pipeline.stages.takeRight(3).head.asInstanceOf[VectorAssembler].getInputCols 697 | val treesReport = ModelDecisionExtractor.extractTreesData(builtModel, features) 698 | ModelDecisionExtractor.blockReplaceJSONText(treesReport, features) 699 | 700 | // COMMAND ---------- 701 | 702 | // MAGIC %md Useful? Sure, I guess. Should something like this be in your ML solution code base? That would be a resounding 'no'.
703 | // MAGIC ```[NOTE]```: the model is garbage. It's supposed to be. We generated silly data that utilizes random sampling with a common seed value. If you see a modeling solution that is using real world data that can achieve 100% classification accuracy, you're either leaking your label or the problem is so simple that you shouldn't be using supervised ML to solve it 704 | 705 | // COMMAND ---------- 706 | 707 | // MAGIC %md ### Listing 13.3 Imperative casting 708 | // MAGIC In the following few listings, we're going to look at some relatively innocuous operations (the casting of a data type within a Spark DataFrame) through the lens of different approaches.
709 | // MAGIC In this listing, we'll see the imperative design that is generally used for demonstration in API Docs. It's clear, concise, easy to follow, and as simple of an approach that is possible. The only downside to it is that the constants are hard-coded in the code, which can prove complex if these references are spread throughout a large code base. 710 | 711 | // COMMAND ---------- 712 | 713 | def simple(df: DataFrame): DataFrame = { 714 | df.withColumn("age", col("age").cast("double")) 715 | .withColumn("weight", col("weight").cast("double")) 716 | .withColumn("hungry", col("hungry").cast("string")) 717 | } 718 | 719 | // COMMAND ---------- 720 | 721 | dataLarger.printSchema 722 | 723 | // COMMAND ---------- 724 | 725 | simple(dataLarger).printSchema 726 | 727 | // COMMAND ---------- 728 | 729 | // MAGIC %md ###Listing 13.4 A hacker's attempt at casting columns 730 | // MAGIC Here we see some pretty unintelligble code. It's overly complex not due to what it's doing, but rather in how it's doing it. It's also incredibly non-performant by using a mutable object definition around the DataFrame. The usage of the function is also merely migrating the constant values of column and type references to another place in the code. This does nothing to solve any problems and is just indicative of someone trying to look smart and failing in an epic manner.
731 | // MAGIC Be on the lookout for implementations like this. If the code is introducing complexity without reducing the computational or space complexity, then it's just useless code that's hard to read, hard to debug, and really challenging to expand upon. 732 | 733 | // COMMAND ---------- 734 | 735 | def hacker(df: DataFrame, castChanges: List[(String, String)]): DataFrame = { 736 | var mutated = df 737 | castChanges.foreach { x => 738 | mutated = mutated.withColumn(x._1, mutated(x._1).cast(x._2)) 739 | } 740 | mutated 741 | } 742 | 743 | // COMMAND ---------- 744 | 745 | hacker(dataLarger, List(("age", "double"), ("weight", "double"), ("hungry", "string"))).printSchema 746 | 747 | // COMMAND ---------- 748 | 749 | // MAGIC %md Yikes! Look at that function signature! Who wants to pass in a list of string tuples to a function?! That's just insanity. 750 | 751 | // COMMAND ---------- 752 | 753 | // MAGIC %md ###Listing 13.5 A pure functional programming approach 754 | // MAGIC Here we have 'the mystic'. Someone who is attempting to adhere to a set of design principles (in this case, FP, but in other cases it might be a strict adherence to a particular design theory (e.g. factory patterns) in OO). The code is cleaner than the hacker's code, certainly, but it's still really challenging to call the function due to the weird nature of using a collection of tuples. 755 | 756 | // COMMAND ---------- 757 | 758 | def mystic(df: DataFrame, 759 | castChanges: List[(String, DataType)]): DataFrame = { 760 | castChanges.foldLeft(df) { 761 | case (data, (c, t)) => data.withColumn(c, df(c).cast(t)) 762 | } 763 | } 764 | 765 | // COMMAND ---------- 766 | 767 | mystic(dataLarger, List(("age", DoubleType), ("weight", DoubleType), ("hungry", StringType))).printSchema 768 | 769 | // COMMAND ---------- 770 | 771 | // MAGIC %md The implementation is clearly better than the previous 2 listings, but the signature is still just relocating complexity to a different place. There are also a LOT of things that could go wrong here if the end-user isn't aware of how to perform casting conversion to complex types (Arrays, Maps, etc.). 772 | 773 | // COMMAND ---------- 774 | 775 | // MAGIC %md ###Listing 13.6 The show-off's casting implementation 776 | // MAGIC Here we have someone who really, desperately wants for people to think that they're skilled. They're going to approach the problem in a way that they think is going to cover a lot of potential issues that might arise from converting types, but in the process of building their solution just ends up going down a rabbit hole of complexity that generates a confusing mess of temporary references and bloated redundant code. 777 | 778 | // COMMAND ---------- 779 | 780 | final val numTypes = List(IntegerType, FloatType, DoubleType, LongType, DecimalType, ShortType) 781 | 782 | def showOff(df: DataFrame): DataFrame = { 783 | df.schema 784 | .map( 785 | s => 786 | s.dataType match { 787 | case x if numTypes.contains(x) => s.name -> "n" 788 | case _ => s.name -> "s" 789 | } 790 | ) 791 | .foldLeft(df) { 792 | case (df, x) => 793 | df.withColumn(x._1, df(x._1).cast(x._2 match { 794 | case "n" => "double" 795 | case _ => "string" 796 | })) 797 | } 798 | } 799 | 800 | // COMMAND ---------- 801 | 802 | showOff(dataLarger).printSchema 803 | 804 | // COMMAND ---------- 805 | 806 | // MAGIC %md While the approach here is better from an instantiation standpoint (namely that it doesn't require configuring a list of tuples and tries to automate conversions to support the goal of casting numeric types to Doubles and the Boolean type of the target column to a string), the internals of the showOff function are convoluted and unnecessarily complex. The first map over the schema is completely useless (creating a mapping of the name to a temporary string representation of the data type) and the foldLeft utilizes positional notation for referring to the values within the map. Not to mention that this implementation will absolutely detonate if any complex types are present in the DataFrame (Array, Map, Vector) and create confusing exceptions for an end-user of the function. It's sloppy, amateurish, and incredibly complicated to read. F- for effort. 807 | 808 | // COMMAND ---------- 809 | 810 | // MAGIC %md ###Listing 13.7 A slightly more sophisticated casting implementation 811 | // MAGIC This code block is approaching something more akin to what a production-grade implementation for automating casting should look like. As I mentioned in the chapter, though, this in and of itself can be highly obfuscated if no one else on the team is familiar with what is going on here. Because it's so dense, efficient, and involves matching directly on the iterated collection of schema values, it can be very confusing for people who are more accustomed to imperative programming styles. 812 | // MAGIC As I mentioned in the chapter, it's important to verify that the team is familiar with FP programming styles such as this before just blindly submitting PR's that contain code like this. If this is the development standard that the team chooses to employ, make sure that the entire team is taught and mentored in this paradigm. Provide examples of simple use cases that are relevant to the type of operations that project work will require. Hold hackathons. Just make sure that you're not smugly expecting people to figure something like this out with no guidance. 813 | 814 | // COMMAND ---------- 815 | 816 | def madScientist(df: DataFrame): DataFrame = { 817 | df.schema.foldLeft(df) { 818 | case (accum, s) => 819 | accum.withColumn(s.name, accum(s.name).cast(s.dataType match { 820 | case x: IntegerType => x 821 | case x if numTypes.contains(x) => DoubleType 822 | case ArrayType(_,_) | MapType(_,_,_) => s.dataType 823 | case _ => StringType 824 | })) 825 | } 826 | } 827 | 828 | // COMMAND ---------- 829 | 830 | madScientist(dataLarger).printSchema 831 | 832 | // COMMAND ---------- 833 | 834 | // MAGIC %md ###Listing 13.8 Configuration and common structures for data generator 835 | // MAGIC This is the same code as above (before listing 13.1) repeated here for reference. 836 | 837 | // COMMAND ---------- 838 | 839 | import org.apache.spark.sql.functions._ 840 | import org.apache.spark.sql.types._ 841 | import org.apache.spark.sql.{DataFrame, SparkSession} 842 | import scala.collection.mutable.ArrayBuffer 843 | import scala.reflect.ClassTag 844 | import scala.util.Random 845 | 846 | case class Dogs(age: Int, 847 | weight: Double, 848 | favorite_food: String, 849 | breed: String, 850 | good_boy_or_girl: String, 851 | hungry: Boolean) 852 | 853 | case object CoreData { 854 | def dogBreeds: Seq[String] = Seq( 855 | "Husky", "GermanShepherd", "Dalmation", "Pug", "Malamute", "Akita", 856 | "BelgianMalinois", "Chinook", "Estrela", "Doberman", "Mastiff") 857 | def foods: Seq[String] = Seq("Kibble", "Spaghetti", "Labneh", "Steak", 858 | "Hummus", "Fajitas", "BeoufBourgignon", "Bolognese") 859 | def goodness: Seq[String] = Seq("yes", "no", "sometimes", "yesWhenFoodAvailable") 860 | def hungry: Seq[Boolean] = Seq(true, false) 861 | def ageSigma = 3 862 | def ageMean = 2 863 | def weightSigma = 12 864 | def weightMean = 60 865 | } 866 | 867 | trait DogUtility { 868 | lazy val spark: SparkSession = SparkSession.builder().getOrCreate() 869 | def getDoggoData[T: ClassTag](a: Seq[T], dogs: Int, seed: Long): Seq[T] = { 870 | val rnd = new Random(seed) 871 | Seq.fill(dogs)(a(rnd.nextInt(a.size))) 872 | } 873 | def getDistributedIntData(sigma: Double, 874 | mean: Double, 875 | dogs: Int, 876 | seed: Long): Seq[Int] = { 877 | val rnd = new Random(seed) 878 | (0 until dogs).map( 879 | _ => math.ceil(math.abs(rnd.nextGaussian() * sigma + mean)).toInt 880 | ) 881 | } 882 | def getDistributedDoubleData(sigma: Double, mean: Double, dogs: Int, 883 | seed: Long): Seq[Double] = { 884 | val rnd = new Random(seed) 885 | (0 until dogs).map( _ => math.round(math.abs(rnd.nextGaussian() * sigma * 100 + mean)) 886 | .toDouble / 100) 887 | } 888 | } 889 | 890 | // COMMAND ---------- 891 | 892 | // MAGIC %md ###Listing 13.9 An overly complex and incorrectly optimized data generator 893 | // MAGIC Here we're looking at an eager early-optimization effort. Individual portions of it are optimized for performance (the generators for individual univariate series), but the key part of the final section (the creation of the structures to be cast into a Spark DataFrame) are incredibly non-performant. Each iteration through the collection forces a scan of the Sequence from the beginning, making the total runtime complexity of this approach somehting close to O(n * log(n)). 894 | 895 | // COMMAND ---------- 896 | 897 | 898 | object PrematureOptimization extends DogUtility { 899 | 900 | import spark.implicits._ 901 | 902 | case class DogInfo(columnName: String, 903 | stringData: Either[Seq[String], Seq[Boolean]], 904 | sigmaData: Option[Double], 905 | meanData: Option[Double], 906 | valueType: String) 907 | 908 | def dogDataConstruct: Seq[DogInfo] = { 909 | Seq( 910 | DogInfo( 911 | "age", 912 | Left(Seq("")), 913 | Some(CoreData.ageSigma), 914 | Some(CoreData.ageMean), 915 | "Int" 916 | ), 917 | DogInfo( 918 | "weight", 919 | Left(Seq("")), 920 | Some(CoreData.weightSigma), 921 | Some(CoreData.weightMean), 922 | "Double" 923 | ), 924 | DogInfo("food", Left(CoreData.foods), Some(0.0), Some(0.0), "String"), 925 | DogInfo( 926 | "breed", 927 | Left(CoreData.dogBreeds), 928 | Some(0.0), 929 | Some(0.0), 930 | "String" 931 | ), 932 | DogInfo("good", Left(CoreData.goodness), Some(0.0), Some(0.0), "String"), 933 | DogInfo( 934 | "hungry", 935 | Right(CoreData.hungry), 936 | Some(CoreData.ageSigma), 937 | Some(CoreData.ageMean), 938 | "Boolean" 939 | ) 940 | ) 941 | } 942 | 943 | def generateOptimizedData(rows: Int, seed: Long): DataFrame = { 944 | 945 | val data = dogDataConstruct 946 | .map( 947 | x => 948 | x.columnName -> { 949 | x.valueType match { 950 | case "Int" => 951 | getDistributedIntData( 952 | x.sigmaData.get, 953 | x.meanData.get, 954 | rows, 955 | seed 956 | ) 957 | case "Double" => 958 | getDistributedDoubleData( 959 | x.sigmaData.get, 960 | x.meanData.get, 961 | rows, 962 | seed 963 | ) 964 | case "String" => getDoggoData(x.stringData.left.get, rows, seed) 965 | case _ => getDoggoData(x.stringData.right.get, rows, seed) 966 | } 967 | } // This should be cast to an IndexedSeq in order to speed things up 968 | ) 969 | .toMap 970 | val collection = (0 until rows).toArray 971 | .map(x => { 972 | Dogs( 973 | data("age")(x).asInstanceOf[Int], 974 | data("weight")(x).asInstanceOf[Double], 975 | data("food")(x).asInstanceOf[String], 976 | data("breed")(x).asInstanceOf[String], 977 | data("good")(x).asInstanceOf[String], 978 | data("hungry")(x).asInstanceOf[Boolean] 979 | ) 980 | }) 981 | .toSeq 982 | collection.toDF() 983 | } 984 | 985 | } 986 | 987 | // COMMAND ---------- 988 | 989 | // MAGIC %md Let's demonstrate how terrible this is for performance. 990 | 991 | // COMMAND ---------- 992 | 993 | PrematureOptimization.generateOptimizedData(5000, 42L) 994 | 995 | // COMMAND ---------- 996 | 997 | PrematureOptimization.generateOptimizedData(50000, 42L) 998 | 999 | // COMMAND ---------- 1000 | 1001 | // MAGIC %md Setting aside the failure in proper optimization, the fact that this data generator is built early on in a project for testing of performance means that, throughout the experimentation phases of feature generation and testing, this code is going to need to be constantly updated and refactored. What happens when different data types are added? What happens when interactions between features would like to be explored? In order to generate the data within this code base, extensive refactoring to the point of needing a full rewrite will be occurring often and taking a long time to execute. 1002 | 1003 | // COMMAND ---------- 1004 | 1005 | // MAGIC %md ###Listing 13.10 A far more performant data generator 1006 | // MAGIC This is a bit different from the generator defined at the top of this notebook. It is both more performant than that one and certainly faster to execute than the preceding one in listing 13.9. The main difference in this one is the significantly simpler generation of the individual series (which could actually use a bit more of a refactor to make it truly useable for rapidly changing data format change testing) and only a single location to update for the generated schema definition. 1007 | 1008 | // COMMAND ---------- 1009 | 1010 | object ConfusingButOptimizedDogData extends DogUtility { 1011 | 1012 | import spark.implicits._ 1013 | 1014 | private def generateCollections(rows: Int, seed: Long): ArrayBuffer[Seq[Any]] = { 1015 | 1016 | var collections = new ArrayBuffer[Seq[Any]]() 1017 | 1018 | collections += getDistributedIntData(CoreData.ageSigma, CoreData.ageMean, rows, seed) 1019 | 1020 | collections += getDistributedDoubleData(CoreData.weightSigma, CoreData.weightMean, rows, seed) 1021 | 1022 | Seq(CoreData.foods, CoreData.dogBreeds, CoreData.goodness, CoreData.hungry) 1023 | .foreach(x => { collections += getDoggoData(x, rows, seed)}) 1024 | 1025 | collections 1026 | } 1027 | def buildDogDF(rows: Int, seed: Long): DataFrame = { 1028 | 1029 | val data = generateCollections(rows, seed) 1030 | 1031 | data.flatMap(_.zipWithIndex) 1032 | .groupBy(_._2).values.map( x => 1033 | Dogs( 1034 | x(0)._1.asInstanceOf[Int], 1035 | x(1)._1.asInstanceOf[Double], 1036 | x(2)._1.asInstanceOf[String], 1037 | x(3)._1.asInstanceOf[String], 1038 | x(4)._1.asInstanceOf[String], 1039 | x(5)._1.asInstanceOf[Boolean])).toSeq.toDF() 1040 | .withColumn("hungry", when(col("hungry"), "true").otherwise("false")) 1041 | .withColumn("hungry", when(col("breed") === "Husky", "true").otherwise(col("hungry"))) 1042 | .withColumn("good_boy_or_girl", when(col("breed") === "Husky", "yesWhenFoodAvailable") 1043 | .otherwise(col("good_boy_or_girl"))) 1044 | } 1045 | } 1046 | 1047 | // COMMAND ---------- 1048 | 1049 | ConfusingButOptimizedDogData.buildDogDF(5000, 42L) 1050 | 1051 | // COMMAND ---------- 1052 | 1053 | ConfusingButOptimizedDogData.buildDogDF(50000, 42L) 1054 | -------------------------------------------------------------------------------- /notebooks/ch16/Chapter16_1.dbc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenWilson2/ML-Engineering/0fc05f4b876b26bbacc85bcb11c7c2aef517cd20/notebooks/ch16/Chapter16_1.dbc -------------------------------------------------------------------------------- /notebooks/ch16/Chapter16_2.dbc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BenWilson2/ML-Engineering/0fc05f4b876b26bbacc85bcb11c7c2aef517cd20/notebooks/ch16/Chapter16_2.dbc --------------------------------------------------------------------------------