{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": { "id": "Q6ifg03dKPR4" }, "outputs": [], "source": [ "############################\n", "# BLOCK 1: IMPORTS\n", "############################\n", "\n", "# libraries!\n", "import numpy as np # numpy is Python's \"array\" library\n", "import pandas as pd # Pandas is Python's \"data\" library (\"dataframe\" == spreadsheet)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": 2, "metadata": { "id": "0ghtg7ecRQ50" }, "outputs": [], "source": [ "#############################\n", "# BLOCK 2: VARIABLES LISTING\n", "#############################\n", "\n", "# for reference, as you work throughout, come back and list all the variable names\n", "# here along with what that variable holds\n", "\n", "# Variable: Contents\n", "# -------------------\n", "# [FILL IN VARS BELOW]\n", "#" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 424 }, "id": "DjCaxH3BKJ_O", "outputId": "ab33e45d-d332-412e-db2a-88cc4c9528f4" }, "outputs": [], "source": [ "####################################\n", "#from sklearn.datasets import load_iris\n", "#data = load_iris()\n", "#df = pd.DataFrame(data.data, columns=data.feature_names)\n", "#df" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "id": "ANJhDRNPKPR5" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "iris.csv: file read into a pandas DataFrame.\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
sepallensepalwidpetallenpetalwidirisnamejunk
0NaN3.51.40.2setosaremove_me
14.93.01.40.2setosaremove_me
24.73.21.30.2setosaremove_me
34.63.11.50.2setosaremove_me
45.03.61.40.2setosaremove_me
\n", "
" ], "text/plain": [ " sepallen sepalwid petallen petalwid irisname junk\n", "0 NaN 3.5 1.4 0.2 setosa remove_me\n", "1 4.9 3.0 1.4 0.2 setosa remove_me\n", "2 4.7 3.2 1.3 0.2 setosa remove_me\n", "3 4.6 3.1 1.5 0.2 setosa remove_me\n", "4 5.0 3.6 1.4 0.2 setosa remove_me" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#############################\n", "# BLOCK 3: READING DATA\n", "#############################\n", "\n", "# let's read in our flower data...\n", "#\n", "filename = 'iris.csv'\n", "df = pd.read_csv(filename, header=0) # encoding=\"latin1\" et al.\n", "print(f\"{filename}: file read into a pandas DataFrame.\")\n", "df.head()" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 361 }, "id": "8v3oVZYTKPR6", "outputId": "4110ac58-5c52-4cbf-a83f-16d9a7189ccb" }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
sepallensepalwidpetallenpetalwidirisnamejunk
0NaN3.51.40.2setosaremove_me
14.93.01.40.2setosaremove_me
24.73.21.30.2setosaremove_me
34.63.11.50.2setosaremove_me
.....................
1466.32.55.01.9virginicaremove_me
1476.53.05.22.0virginicaremove_me
1486.23.45.42.3alienirisremove_me
1495.93.05.11.8alienirisremove_me
\n", "

150 rows × 6 columns

\n", "
" ], "text/plain": [ " sepallen sepalwid petallen petalwid irisname junk\n", "0 NaN 3.5 1.4 0.2 setosa remove_me\n", "1 4.9 3.0 1.4 0.2 setosa remove_me\n", "2 4.7 3.2 1.3 0.2 setosa remove_me\n", "3 4.6 3.1 1.5 0.2 setosa remove_me\n", ".. ... ... ... ... ... ...\n", "146 6.3 2.5 5.0 1.9 virginica remove_me\n", "147 6.5 3.0 5.2 2.0 virginica remove_me\n", "148 6.2 3.4 5.4 2.3 alieniris remove_me\n", "149 5.9 3.0 5.1 1.8 alieniris remove_me\n", "\n", "[150 rows x 6 columns]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "#############################\n", "# BLOCK 4: SETTING OPTIONS\n", "#############################\n", "\n", "#\n", "# a dataframe is a \"spreadsheet in Python\"\n", "# (this one seems to have an extra column!)\n", "#\n", "pd.set_option('display.max_rows', 8) # None for no limit; default: 10\n", "pd.set_option('display.min_rows', 8) # None for no limit; default: 10\n", "# let's view it!\n", "df" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "aksGQcJ8KPR6", "outputId": "4cbdedf5-cabe-4263-c06c-26bbb8f509b1" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "RangeIndex: 150 entries, 0 to 149\n", "Data columns (total 6 columns):\n", " # Column Non-Null Count Dtype \n", "--- ------ -------------- ----- \n", " 0 sepallen 149 non-null float64\n", " 1 sepalwid 150 non-null float64\n", " 2 petallen 150 non-null float64\n", " 3 petalwid 149 non-null float64\n", " 4 irisname 149 non-null str \n", " 5 junk 150 non-null str \n", "dtypes: float64(4), str(2)\n", "memory usage: 7.2 KB\n" ] } ], "source": [ "##############################\n", "# BLOCK 5: USING DF'S .info()\n", "##############################\n", "\n", "#\n", "# let's look at our pandas DataFrame's info (Aargh: that extra column!)\n", "#\n", "df.info()" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "ADWpUqeKKPR7", "outputId": "76ac8d2f-0d98-4721-e0f2-b79b48736b00" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "RangeIndex: 150 entries, 0 to 149\n", "Data columns (total 5 columns):\n", " # Column Non-Null Count Dtype \n", "--- ------ -------------- ----- \n", " 0 sepallen 149 non-null float64\n", " 1 sepalwid 150 non-null float64\n", " 2 petallen 150 non-null float64\n", " 3 petalwid 149 non-null float64\n", " 4 irisname 149 non-null str \n", "dtypes: float64(4), str(1)\n", "memory usage: 6.0 KB\n" ] } ], "source": [ "#############################\n", "# BLOCK 6: CLEANING DATA\n", "#############################\n", "\n", "#\n", "# let's drop that last column (dropping is usually by _name_):\n", "#\n", "# if you want a list of the column names use df.columns\n", "#col5name = df.columns[5] # get column name at index 5\n", "\n", "df_clean = df.drop(columns=['junk']) # drop by name is typical, but what else is possible?\n", "df_clean.info() # Is the bad last column gone?" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "9RAydRsiKPR7", "outputId": "18ec37ec-3a95-4ef9-c111-847ac8f6ecec" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "FEATURES: Index(['sepallen', 'sepalwid', 'petallen', 'petalwid', 'irisname'], dtype='str')\n", "\n", "First feature: sepallen\n", "\n", "feature_name_to_index: {'sepallen': 0, 'sepalwid': 1, 'petallen': 2, 'petalwid': 3, 'irisname': 4}\n" ] } ], "source": [ "##############################\n", "# BLOCK 7: FEATURE NAMES DICT\n", "##############################\n", "\n", "#\n", "# let's keep our column names in variables, for reference;\n", "# in machine learning contexts, these are referred to as \"features\"\n", "#\n", "features = df_clean.columns # \"list\" of columns\n", "print(f\"FEATURES: {features}\\n\")\n", " # It's a \"pandas\" list, called an Index\n", " # use it just as a Python list of strings:\n", "print(f\"First feature: {features[0]}\\n\")\n", "\n", "# let's create a dictionary to look up any column index by name\n", "feature_name_to_index = {}\n", "for i, name in enumerate(features):\n", " feature_name_to_index[name] = i # using the name (as key), assign the value (i)\n", "print(f\"feature_name_to_index: {feature_name_to_index}\")" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 1000 }, "id": "PWkPTOGnKPR8", "outputId": "f6a46e8f-0169-4646-864f-1b3ed749f238" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "RangeIndex: 150 entries, 0 to 149\n", "Data columns (total 5 columns):\n", " # Column Non-Null Count Dtype \n", "--- ------ -------------- ----- \n", " 0 sepallen 149 non-null float64\n", " 1 sepalwid 150 non-null float64\n", " 2 petallen 150 non-null float64\n", " 3 petalwid 149 non-null float64\n", " 4 irisname 149 non-null str \n", "dtypes: float64(4), str(1)\n", "memory usage: 6.0 KB\n", "======================================================================\n", " sepallen sepalwid petallen petalwid irisname\n", "0 NaN 3.5 1.4 0.2 setosa\n", "1 4.9 3.0 1.4 0.2 setosa\n", "2 4.7 3.2 1.3 0.2 setosa\n", "3 4.6 3.1 1.5 0.2 setosa\n", ".. ... ... ... ... ...\n", "146 6.3 2.5 5.0 1.9 virginica\n", "147 6.5 3.0 5.2 2.0 virginica\n", "148 6.2 3.4 5.4 2.3 alieniris\n", "149 5.9 3.0 5.1 1.8 alieniris\n", "\n", "[150 rows x 5 columns]\n", "======================================================================\n", "Irisname entries: \n", "['setosa', nan, 'versicolor', 'virginica', 'alieniris']\n", "Length: 5, dtype: str\n" ] } ], "source": [ "##############################\n", "# BLOCK 8: INSPECTING DATA\n", "##############################\n", "\n", "#\n", "# let's look at our cleaned-up dataframe...\n", "#\n", "df_clean.info()\n", "print('=' * 70)\n", "\n", "#\n", "# Notice that the non-null count is _different_ for irisname!\n", "# Why? Show a table and inspect...\n", "print(df_clean)\n", "print('=' * 70)\n", "\n", "# or more to the point, grab that column and inspect:\n", "print(f\"Irisname entries: {df_clean['irisname'].unique()}\")\n", "\n", "# also note how the last two rows in the df have problems, among others...\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "4B2An3iML1ay", "outputId": "5492db65-acd8-4c11-f669-742a79b6cb62" }, "outputs": [ { "data": { "text/plain": [ "Index(['sepallen', 'sepalwid', 'petallen', 'petalwid', 'irisname', 'junk'], dtype='str')" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df.columns" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "id": "xM22NWdqKPR-", "scrolled": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Index: 147 entries, 1 to 149\n", "Data columns (total 5 columns):\n", " # Column Non-Null Count Dtype \n", "--- ------ -------------- ----- \n", " 0 sepallen 147 non-null float64\n", " 1 sepalwid 147 non-null float64\n", " 2 petallen 147 non-null float64\n", " 3 petalwid 147 non-null float64\n", " 4 irisname 147 non-null str \n", "dtypes: float64(4), str(1)\n", "memory usage: 6.9 KB\n", "======================================================================\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
sepallensepalwidpetallenpetalwidirisname
14.93.01.40.2setosa
24.73.21.30.2setosa
34.63.11.50.2setosa
45.03.61.40.2setosa
..................
1466.32.55.01.9virginica
1476.53.05.22.0virginica
1486.23.45.42.3alieniris
1495.93.05.11.8alieniris
\n", "

147 rows × 5 columns

\n", "
" ], "text/plain": [ " sepallen sepalwid petallen petalwid irisname\n", "1 4.9 3.0 1.4 0.2 setosa\n", "2 4.7 3.2 1.3 0.2 setosa\n", "3 4.6 3.1 1.5 0.2 setosa\n", "4 5.0 3.6 1.4 0.2 setosa\n", ".. ... ... ... ... ...\n", "146 6.3 2.5 5.0 1.9 virginica\n", "147 6.5 3.0 5.2 2.0 virginica\n", "148 6.2 3.4 5.4 2.3 alieniris\n", "149 5.9 3.0 5.1 1.8 alieniris\n", "\n", "[147 rows x 5 columns]" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "##############################\n", "# BLOCK 9: USING DF'S dropna\n", "##############################\n", "\n", "#\n", "# typically, after dropping columns that we don't want,\n", "# we drop rows with missing data (other approaches are possible, too)\n", "#\n", "df_clean = df_clean.dropna() # this removes all rows with nan items\n", "df_clean.info()\n", "print('=' * 70)\n", "df_clean\n", "\n", "#\n", "# notice that _all_ of the rows now have 144 non-null items\n", "# also, the first and last rows (among others) aren't valid data...\n", "# we'll handle that next" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "id": "h0spkxGzKPR-" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Irisname entries: \n", "['setosa', 'versicolor', 'virginica', 'alieniris']\n", "Length: 4, dtype: str\n", "1 False\n", "2 False\n", "3 False\n", "4 False\n", " ... \n", "146 False\n", "147 False\n", "148 True\n", "149 True\n", "Name: irisname, Length: 147, dtype: bool\n", "1 True\n", "2 True\n", "3 True\n", "4 True\n", " ... \n", "146 True\n", "147 True\n", "148 False\n", "149 False\n", "Name: irisname, Length: 147, dtype: bool\n", "(145, 5)\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
sepallensepalwidpetallenpetalwidirisname
14.93.01.40.2setosa
24.73.21.30.2setosa
34.63.11.50.2setosa
45.03.61.40.2setosa
..................
1446.73.35.72.5virginica
1456.73.05.22.3virginica
1466.32.55.01.9virginica
1476.53.05.22.0virginica
\n", "

145 rows × 5 columns

\n", "
" ], "text/plain": [ " sepallen sepalwid petallen petalwid irisname\n", "1 4.9 3.0 1.4 0.2 setosa\n", "2 4.7 3.2 1.3 0.2 setosa\n", "3 4.6 3.1 1.5 0.2 setosa\n", "4 5.0 3.6 1.4 0.2 setosa\n", ".. ... ... ... ... ...\n", "144 6.7 3.3 5.7 2.5 virginica\n", "145 6.7 3.0 5.2 2.3 virginica\n", "146 6.3 2.5 5.0 1.9 virginica\n", "147 6.5 3.0 5.2 2.0 virginica\n", "\n", "[145 rows x 5 columns]" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "################################\n", "# BLOCK 10: REMOVING BOGUS DATA\n", "################################\n", "\n", "# or more to the point, grab that column and inspect:\n", "print(f\"Irisname entries: {df_clean['irisname'].unique()}\")\n", "print(df_clean['irisname'] == 'alieniris') # what does this show?\n", "print(df_clean['irisname'] != 'alieniris') # what does this show?\n", "\n", "# define a final version of the DataFrame by pulling out the\n", "# bad alieniris data (remember that you can pass a boolean\n", "# Series to select data that you want)\n", "df_final = df_clean[df_clean['irisname'] != 'alieniris'].copy()\n", "# ^^^^ YOU NEED TO WRITE CODE HERE...\n", "\n", "print(df_final.shape)\n", "df_final" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "id": "EywtvRyMKPR-" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'setosa': 0, 'versicolor': 1, 'virginica': 2}\n", "\n", "setosa maps to 0\n", "versicolor maps to 1\n", "virginica maps to 2\n" ] } ], "source": [ "##########################################\n", "# BLOCK 11: CONVERT SPECIES NAME TO INDEX\n", "##########################################\n", "\n", "# all of scikit-learn's ML routines need numbers, not strings\n", "# ... even for categories/classifications (like species!)\n", "# so, we will convert the flower-species to numbers:\n", "\n", "species_names = df_final['irisname'].unique()\n", "species_name_to_index = { species_names[i]:i for i in range(len(species_names))}\n", "print(species_name_to_index)\n", "print(type(species_name_to_index))\n", "\n", "def convertSpecies(species_name: str) -> int:\n", " ''' return the species index (a unique integer/category) '''\n", " #print(f\"converting {species_name}...\")\n", " return species_name_to_index[species_name]\n", "\n", "# Let's try it out...\n", "for name in species_names:\n", " print(f\"{name} maps to {convertSpecies(name)}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "NuKqemiiKPR_" }, "outputs": [], "source": [ "##########################################\n", "# BLOCK 12: USING DF'S .apply\n", "##########################################\n", "\n", "#\n", "# we can \"apply\" our new convertSpecies function to a whole column\n", "#\n", "# (The following will issue a \"SettingWithCopyWarning\" here...)\n", "# see https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", "#\n", "# >>> ADD CODE HERE TO KEEP THE WARNING FROM HAPPENING <<<\n", "\n", "df_final.loc[:, 'irisname'] = df_final['irisname'].apply(convertSpecies)\n", "#df_final.loc[:, 'irisname'] = df_final['irisname'].apply(convertSpecies)\n", "# Don't run this twice! Why?! What's \"KeyError: 0\"?\n", "# (of course, you can always go back and re-establish definitions of df_final)\n", "\n" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "id": "tWoVKJBGKPR_" }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
sepallensepalwidpetallenpetalwidirisname
14.93.01.40.20
24.73.21.30.20
34.63.11.50.20
45.03.61.40.20
..................
1446.73.35.72.52
1456.73.05.22.32
1466.32.55.01.92
1476.53.05.22.02
\n", "

145 rows × 5 columns

\n", "
" ], "text/plain": [ " sepallen sepalwid petallen petalwid irisname\n", "1 4.9 3.0 1.4 0.2 0\n", "2 4.7 3.2 1.3 0.2 0\n", "3 4.6 3.1 1.5 0.2 0\n", "4 5.0 3.6 1.4 0.2 0\n", ".. ... ... ... ... ...\n", "144 6.7 3.3 5.7 2.5 2\n", "145 6.7 3.0 5.2 2.3 2\n", "146 6.3 2.5 5.0 1.9 2\n", "147 6.5 3.0 5.2 2.0 2\n", "\n", "[145 rows x 5 columns]" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "##########################################\n", "# BLOCK 13: CONFIRMING FINAL DATAFRAME\n", "##########################################\n", "\n", "#\n", "# let's see it! (this is safe to run many times...)\n", "#\n", "df_final # print(df_final.tostring()) # for _all_ rows..." ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "id": "qxpBs7NnKPR_" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "type of A: \n", " sepallen sepalwid petallen petalwid irisname\n", "1 4.9 3.0 1.4 0.2 0\n", "2 4.7 3.2 1.3 0.2 0\n", "3 4.6 3.1 1.5 0.2 0\n", "4 5.0 3.6 1.4 0.2 0\n", "5 5.4 3.9 1.7 0.4 0\n", "A[0] = [4.9 3. 1.4 0.2 0. ]\n" ] } ], "source": [ "##########################################\n", "# BLOCK 14: CONVERTING TO NUMPY FORMAT\n", "##########################################\n", "\n", "#\n", "# let's convert our dataframe to a numpy array, named A\n", "# Our ML library, scikit-learn operates entirely on numpy arrays.\n", "#\n", "#A = df_final.values\n", "A = df_final.to_numpy() # better -- self-documenting!\n", "\n", "print(f\"type of A: {type(A)}\")\n", "print(df_final.head())\n", "print(f\"A[0] = {A[0]}\")" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "id": "bblbBuIiKPSA" }, "outputs": [ { "data": { "text/plain": [ "array([[4.9, 3. , 1.4, 0.2, 0. ],\n", " [4.7, 3.2, 1.3, 0.2, 0. ],\n", " [4.6, 3.1, 1.5, 0.2, 0. ],\n", " [5. , 3.6, 1.4, 0.2, 0. ],\n", " [5.4, 3.9, 1.7, 0.4, 0. ],\n", " [4.6, 3.4, 1.4, 0.3, 0. ],\n", " [4.4, 2.9, 1.4, 0.2, 0. ],\n", " [4.9, 3.1, 1.5, 0.1, 0. ],\n", " [5.4, 3.7, 1.5, 0.2, 0. ],\n", " [4.8, 3.4, 1.6, 0.2, 0. ],\n", " [4.8, 3. , 1.4, 0.1, 0. ],\n", " [4.3, 3. , 1.1, 0.1, 0. ],\n", " [5.8, 4. , 1.2, 0.2, 0. ],\n", " [5.7, 4.4, 1.5, 0.4, 0. ],\n", " [5.4, 3.9, 1.3, 0.4, 0. ],\n", " [5.1, 3.5, 1.4, 0.3, 0. ],\n", " [5.7, 3.8, 1.7, 0.3, 0. ],\n", " [5.1, 3.8, 1.5, 0.3, 0. ],\n", " [5.4, 3.4, 1.7, 0.2, 0. ],\n", " [5.1, 3.7, 1.5, 0.4, 0. ],\n", " [4.6, 3.6, 1. , 0.2, 0. ],\n", " [5.1, 3.3, 1.7, 0.5, 0. ],\n", " [4.8, 3.4, 1.9, 0.2, 0. ],\n", " [5. , 3.4, 1.6, 0.4, 0. ],\n", " [5.2, 3.5, 1.5, 0.2, 0. ],\n", " [5.2, 3.4, 1.4, 0.2, 0. ],\n", " [4.7, 3.2, 1.6, 0.2, 0. ],\n", " [4.8, 3.1, 1.6, 0.2, 0. ],\n", " [5.4, 3.4, 1.5, 0.4, 0. ],\n", " [5.2, 4.1, 1.5, 0.1, 0. ],\n", " [5.5, 4.2, 1.4, 0.2, 0. ],\n", " [4.9, 3.1, 1.5, 0.2, 0. ],\n", " [5. , 3.2, 1.2, 0.2, 0. ],\n", " [5.5, 3.5, 1.3, 0.2, 0. ],\n", " [4.9, 3.6, 1.4, 0.1, 0. ],\n", " [4.4, 3. , 1.3, 0.2, 0. ],\n", " [5.1, 3.4, 1.5, 0.2, 0. ],\n", " [5. , 3.5, 1.3, 0.3, 0. ],\n", " [4.5, 2.3, 1.3, 0.3, 0. ],\n", " [4.4, 3.2, 1.3, 0.2, 0. ],\n", " [5. , 3.5, 1.6, 0.6, 0. ],\n", " [5.1, 3.8, 1.9, 0.4, 0. ],\n", " [4.8, 3. , 1.4, 0.3, 0. ],\n", " [5.1, 3.8, 1.6, 0.2, 0. ],\n", " [4.6, 3.2, 1.4, 0.2, 0. ],\n", " [5.3, 3.7, 1.5, 0.2, 0. ],\n", " [5. , 3.3, 1.4, 0.2, 0. ],\n", " [7. , 3.2, 4.7, 1.4, 1. ],\n", " [6.4, 3.2, 4.5, 1.5, 1. ],\n", " [6.9, 3.1, 4.9, 1.5, 1. ],\n", " [5.5, 2.3, 4. , 1.3, 1. ],\n", " [6.5, 2.8, 4.6, 1.5, 1. ],\n", " [5.7, 2.8, 4.5, 1.3, 1. ],\n", " [6.3, 3.3, 4.7, 1.6, 1. ],\n", " [4.9, 2.4, 3.3, 1. , 1. ],\n", " [6.6, 2.9, 4.6, 1.3, 1. ],\n", " [5.2, 2.7, 3.9, 1.4, 1. ],\n", " [5. , 2. , 3.5, 1. , 1. ],\n", " [5.9, 3. , 4.2, 1.5, 1. ],\n", " [6. , 2.2, 4. , 1. , 1. ],\n", " [6.1, 2.9, 4.7, 1.4, 1. ],\n", " [5.6, 2.9, 3.6, 1.3, 1. ],\n", " [6.7, 3.1, 4.4, 1.4, 1. ],\n", " [5.6, 3. , 4.5, 1.5, 1. ],\n", " [5.8, 2.7, 4.1, 1. , 1. ],\n", " [6.2, 2.2, 4.5, 1.5, 1. ],\n", " [5.6, 2.5, 3.9, 1.1, 1. ],\n", " [5.9, 3.2, 4.8, 1.8, 1. ],\n", " [6.1, 2.8, 4. , 1.3, 1. ],\n", " [6.3, 2.5, 4.9, 1.5, 1. ],\n", " [6.1, 2.8, 4.7, 1.2, 1. ],\n", " [6.4, 2.9, 4.3, 1.3, 1. ],\n", " [6.6, 3. , 4.4, 1.4, 1. ],\n", " [6.8, 2.8, 4.8, 1.4, 1. ],\n", " [6.7, 3. , 5. , 1.7, 1. ],\n", " [6. , 2.9, 4.5, 1.5, 1. ],\n", " [5.7, 2.6, 3.5, 1. , 1. ],\n", " [5.5, 2.4, 3.8, 1.1, 1. ],\n", " [5.5, 2.4, 3.7, 1. , 1. ],\n", " [5.8, 2.7, 3.9, 1.2, 1. ],\n", " [6. , 2.7, 5.1, 1.6, 1. ],\n", " [5.4, 3. , 4.5, 1.5, 1. ],\n", " [6. , 3.4, 4.5, 1.6, 1. ],\n", " [6.7, 3.1, 4.7, 1.5, 1. ],\n", " [6.3, 2.3, 4.4, 1.3, 1. ],\n", " [5.6, 3. , 4.1, 1.3, 1. ],\n", " [5.5, 2.5, 4. , 1.3, 1. ],\n", " [5.5, 2.6, 4.4, 1.2, 1. ],\n", " [6.1, 3. , 4.6, 1.4, 1. ],\n", " [5.8, 2.6, 4. , 1.2, 1. ],\n", " [5. , 2.3, 3.3, 1. , 1. ],\n", " [5.6, 2.7, 4.2, 1.3, 1. ],\n", " [5.7, 3. , 4.2, 1.2, 1. ],\n", " [5.7, 2.9, 4.2, 1.3, 1. ],\n", " [6.2, 2.9, 4.3, 1.3, 1. ],\n", " [5.1, 2.5, 3. , 1.1, 1. ],\n", " [5.7, 2.8, 4.1, 1.3, 1. ],\n", " [6.3, 3.3, 6. , 2.5, 2. ],\n", " [5.8, 2.7, 5.1, 1.9, 2. ],\n", " [7.1, 3. , 5.9, 2.1, 2. ],\n", " [6.3, 2.9, 5.6, 1.8, 2. ],\n", " [6.5, 3. , 5.8, 2.2, 2. ],\n", " [7.6, 3. , 6.6, 2.1, 2. ],\n", " [4.9, 2.5, 4.5, 1.7, 2. ],\n", " [7.3, 2.9, 6.3, 1.8, 2. ],\n", " [6.7, 2.5, 5.8, 1.8, 2. ],\n", " [7.2, 3.6, 6.1, 2.5, 2. ],\n", " [6.5, 3.2, 5.1, 2. , 2. ],\n", " [6.4, 2.7, 5.3, 1.9, 2. ],\n", " [6.8, 3. , 5.5, 2.1, 2. ],\n", " [5.7, 2.5, 5. , 2. , 2. ],\n", " [5.8, 2.8, 5.1, 2.4, 2. ],\n", " [6.4, 3.2, 5.3, 2.3, 2. ],\n", " [6.5, 3. , 5.5, 1.8, 2. ],\n", " [7.7, 3.8, 6.7, 2.2, 2. ],\n", " [7.7, 2.6, 6.9, 2.3, 2. ],\n", " [6. , 2.2, 5. , 1.5, 2. ],\n", " [6.9, 3.2, 5.7, 2.3, 2. ],\n", " [5.6, 2.8, 4.9, 2. , 2. ],\n", " [7.7, 2.8, 6.7, 2. , 2. ],\n", " [6.3, 2.7, 4.9, 1.8, 2. ],\n", " [6.7, 3.3, 5.7, 2.1, 2. ],\n", " [7.2, 3.2, 6. , 1.8, 2. ],\n", " [6.2, 2.8, 4.8, 1.8, 2. ],\n", " [6.1, 3. , 4.9, 1.8, 2. ],\n", " [6.4, 2.8, 5.6, 2.1, 2. ],\n", " [7.2, 3. , 5.8, 1.6, 2. ],\n", " [7.4, 2.8, 6.1, 1.9, 2. ],\n", " [7.9, 3.8, 6.4, 2. , 2. ],\n", " [6.4, 2.8, 5.6, 2.2, 2. ],\n", " [6.3, 2.8, 5.1, 1.5, 2. ],\n", " [6.1, 2.6, 5.6, 1.4, 2. ],\n", " [7.7, 3. , 6.1, 2.3, 2. ],\n", " [6.3, 3.4, 5.6, 2.4, 2. ],\n", " [6.4, 3.1, 5.5, 1.8, 2. ],\n", " [6. , 3. , 4.8, 1.8, 2. ],\n", " [6.9, 3.1, 5.4, 2.1, 2. ],\n", " [6.7, 3.1, 5.6, 2.4, 2. ],\n", " [6.9, 3.1, 5.1, 2.3, 2. ],\n", " [5.8, 2.7, 5.1, 1.9, 2. ],\n", " [6.8, 3.2, 5.9, 2.3, 2. ],\n", " [6.7, 3.3, 5.7, 2.5, 2. ],\n", " [6.7, 3. , 5.2, 2.3, 2. ],\n", " [6.3, 2.5, 5. , 1.9, 2. ],\n", " [6.5, 3. , 5.2, 2. , 2. ]])" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "##################################################\n", "# BLOCK 15: CONVERTING NUMPY ARRAY TO ALL FLOATS\n", "##################################################\n", "\n", "#\n", "# let's convert to make sure it's all floating-point, so we can multiply and divide\n", "#\n", "A = A.astype('float64') # so many: www.tutorialspoint.com/numpy/numpy_data_types.htm\n", "A" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "id": "wbns1qpJKPSA" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "The dataset has 145 rows and 5 cols\n", "[[4.9 3. 1.4 0.2 0. ]\n", " [4.7 3.2 1.3 0.2 0. ]\n", " [4.6 3.1 1.5 0.2 0. ]\n", " [5. 3.6 1.4 0.2 0. ]\n", " [5.4 3.9 1.7 0.4 0. ]\n", " [4.6 3.4 1.4 0.3 0. ]\n", " [4.4 2.9 1.4 0.2 0. ]\n", " [4.9 3.1 1.5 0.1 0. ]\n", " [5.4 3.7 1.5 0.2 0. ]\n", " [4.8 3.4 1.6 0.2 0. ]\n", " [4.8 3. 1.4 0.1 0. ]\n", " [4.3 3. 1.1 0.1 0. ]\n", " [5.8 4. 1.2 0.2 0. ]\n", " [5.7 4.4 1.5 0.4 0. ]\n", " [5.4 3.9 1.3 0.4 0. ]\n", " [5.1 3.5 1.4 0.3 0. ]\n", " [5.7 3.8 1.7 0.3 0. ]\n", " [5.1 3.8 1.5 0.3 0. ]\n", " [5.4 3.4 1.7 0.2 0. ]\n", " [5.1 3.7 1.5 0.4 0. ]\n", " [4.6 3.6 1. 0.2 0. ]\n", " [5.1 3.3 1.7 0.5 0. ]\n", " [4.8 3.4 1.9 0.2 0. ]\n", " [5. 3.4 1.6 0.4 0. ]\n", " [5.2 3.5 1.5 0.2 0. ]\n", " [5.2 3.4 1.4 0.2 0. ]\n", " [4.7 3.2 1.6 0.2 0. ]\n", " [4.8 3.1 1.6 0.2 0. ]\n", " [5.4 3.4 1.5 0.4 0. ]\n", " [5.2 4.1 1.5 0.1 0. ]\n", " [5.5 4.2 1.4 0.2 0. ]\n", " [4.9 3.1 1.5 0.2 0. ]\n", " [5. 3.2 1.2 0.2 0. ]\n", " [5.5 3.5 1.3 0.2 0. ]\n", " [4.9 3.6 1.4 0.1 0. ]\n", " [4.4 3. 1.3 0.2 0. ]\n", " [5.1 3.4 1.5 0.2 0. ]\n", " [5. 3.5 1.3 0.3 0. ]\n", " [4.5 2.3 1.3 0.3 0. ]\n", " [4.4 3.2 1.3 0.2 0. ]\n", " [5. 3.5 1.6 0.6 0. ]\n", " [5.1 3.8 1.9 0.4 0. ]\n", " [4.8 3. 1.4 0.3 0. ]\n", " [5.1 3.8 1.6 0.2 0. ]\n", " [4.6 3.2 1.4 0.2 0. ]\n", " [5.3 3.7 1.5 0.2 0. ]\n", " [5. 3.3 1.4 0.2 0. ]\n", " [7. 3.2 4.7 1.4 1. ]\n", " [6.4 3.2 4.5 1.5 1. ]\n", " [6.9 3.1 4.9 1.5 1. ]\n", " [5.5 2.3 4. 1.3 1. ]\n", " [6.5 2.8 4.6 1.5 1. ]\n", " [5.7 2.8 4.5 1.3 1. ]\n", " [6.3 3.3 4.7 1.6 1. ]\n", " [4.9 2.4 3.3 1. 1. ]\n", " [6.6 2.9 4.6 1.3 1. ]\n", " [5.2 2.7 3.9 1.4 1. ]\n", " [5. 2. 3.5 1. 1. ]\n", " [5.9 3. 4.2 1.5 1. ]\n", " [6. 2.2 4. 1. 1. ]\n", " [6.1 2.9 4.7 1.4 1. ]\n", " [5.6 2.9 3.6 1.3 1. ]\n", " [6.7 3.1 4.4 1.4 1. ]\n", " [5.6 3. 4.5 1.5 1. ]\n", " [5.8 2.7 4.1 1. 1. ]\n", " [6.2 2.2 4.5 1.5 1. ]\n", " [5.6 2.5 3.9 1.1 1. ]\n", " [5.9 3.2 4.8 1.8 1. ]\n", " [6.1 2.8 4. 1.3 1. ]\n", " [6.3 2.5 4.9 1.5 1. ]\n", " [6.1 2.8 4.7 1.2 1. ]\n", " [6.4 2.9 4.3 1.3 1. ]\n", " [6.6 3. 4.4 1.4 1. ]\n", " [6.8 2.8 4.8 1.4 1. ]\n", " [6.7 3. 5. 1.7 1. ]\n", " [6. 2.9 4.5 1.5 1. ]\n", " [5.7 2.6 3.5 1. 1. ]\n", " [5.5 2.4 3.8 1.1 1. ]\n", " [5.5 2.4 3.7 1. 1. ]\n", " [5.8 2.7 3.9 1.2 1. ]\n", " [6. 2.7 5.1 1.6 1. ]\n", " [5.4 3. 4.5 1.5 1. ]\n", " [6. 3.4 4.5 1.6 1. ]\n", " [6.7 3.1 4.7 1.5 1. ]\n", " [6.3 2.3 4.4 1.3 1. ]\n", " [5.6 3. 4.1 1.3 1. ]\n", " [5.5 2.5 4. 1.3 1. ]\n", " [5.5 2.6 4.4 1.2 1. ]\n", " [6.1 3. 4.6 1.4 1. ]\n", " [5.8 2.6 4. 1.2 1. ]\n", " [5. 2.3 3.3 1. 1. ]\n", " [5.6 2.7 4.2 1.3 1. ]\n", " [5.7 3. 4.2 1.2 1. ]\n", " [5.7 2.9 4.2 1.3 1. ]\n", " [6.2 2.9 4.3 1.3 1. ]\n", " [5.1 2.5 3. 1.1 1. ]\n", " [5.7 2.8 4.1 1.3 1. ]\n", " [6.3 3.3 6. 2.5 2. ]\n", " [5.8 2.7 5.1 1.9 2. ]\n", " [7.1 3. 5.9 2.1 2. ]\n", " [6.3 2.9 5.6 1.8 2. ]\n", " [6.5 3. 5.8 2.2 2. ]\n", " [7.6 3. 6.6 2.1 2. ]\n", " [4.9 2.5 4.5 1.7 2. ]\n", " [7.3 2.9 6.3 1.8 2. ]\n", " [6.7 2.5 5.8 1.8 2. ]\n", " [7.2 3.6 6.1 2.5 2. ]\n", " [6.5 3.2 5.1 2. 2. ]\n", " [6.4 2.7 5.3 1.9 2. ]\n", " [6.8 3. 5.5 2.1 2. ]\n", " [5.7 2.5 5. 2. 2. ]\n", " [5.8 2.8 5.1 2.4 2. ]\n", " [6.4 3.2 5.3 2.3 2. ]\n", " [6.5 3. 5.5 1.8 2. ]\n", " [7.7 3.8 6.7 2.2 2. ]\n", " [7.7 2.6 6.9 2.3 2. ]\n", " [6. 2.2 5. 1.5 2. ]\n", " [6.9 3.2 5.7 2.3 2. ]\n", " [5.6 2.8 4.9 2. 2. ]\n", " [7.7 2.8 6.7 2. 2. ]\n", " [6.3 2.7 4.9 1.8 2. ]\n", " [6.7 3.3 5.7 2.1 2. ]\n", " [7.2 3.2 6. 1.8 2. ]\n", " [6.2 2.8 4.8 1.8 2. ]\n", " [6.1 3. 4.9 1.8 2. ]\n", " [6.4 2.8 5.6 2.1 2. ]\n", " [7.2 3. 5.8 1.6 2. ]\n", " [7.4 2.8 6.1 1.9 2. ]\n", " [7.9 3.8 6.4 2. 2. ]\n", " [6.4 2.8 5.6 2.2 2. ]\n", " [6.3 2.8 5.1 1.5 2. ]\n", " [6.1 2.6 5.6 1.4 2. ]\n", " [7.7 3. 6.1 2.3 2. ]\n", " [6.3 3.4 5.6 2.4 2. ]\n", " [6.4 3.1 5.5 1.8 2. ]\n", " [6. 3. 4.8 1.8 2. ]\n", " [6.9 3.1 5.4 2.1 2. ]\n", " [6.7 3.1 5.6 2.4 2. ]\n", " [6.9 3.1 5.1 2.3 2. ]\n", " [5.8 2.7 5.1 1.9 2. ]\n", " [6.8 3.2 5.9 2.3 2. ]\n", " [6.7 3.3 5.7 2.5 2. ]\n", " [6.7 3. 5.2 2.3 2. ]\n", " [6.3 2.5 5. 1.9 2. ]\n", " [6.5 3. 5.2 2. 2. ]]\n" ] } ], "source": [ "##########################################\n", "# BLOCK 16: USING NUMPY'S .shape\n", "##########################################\n", "\n", "#\n", "# nice to have num_rows and num_cols variables handy...\n", "#\n", "num_rows, num_cols = A.shape\n", "print(f\"\\nThe dataset has {num_rows} rows and {num_cols} cols\")\n", "print(A)" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "id": "ixiZhsuqKPSA", "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "flower #132 data is [7.7 3. 6.1 2.3 2. ]\n", " Its sepallen is 7.7\n", " Its sepalwid is 3.0\n", " Its petallen is 6.1\n", " Its petalwid is 2.3\n", " Its irisname is virginica (2)\n" ] } ], "source": [ "##########################################\n", "# BLOCK 17: PRINTING FLOWER INFO\n", "##########################################\n", "\n", "# let's use all of our previously-defined variables, to reinforce names...\n", "\n", "# choose a row index (particular flower) arbitrarily:\n", "flower = 132\n", "print(f\"flower #{flower} data is {A[flower]}\")\n", "\n", "for i in range(len(features)):\n", " col_name = features[i]\n", " if col_name != 'irisname':\n", " print(f\" Its {col_name} is {A[flower][i]}\")\n", " else:\n", " species_num = int(A[flower][i])\n", " species_name = species_names[species_num]\n", " print(f\" Its {col_name} is {species_name} ({species_num})\")" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "id": "izGld2tSKPSA" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "I predict versicolor (1) from features [4.6, 3.6, 3.0, 1.2]\n" ] } ], "source": [ "##########################################\n", "# BLOCK 18: WRITING OUR OWN 1-NN FUNCTION\n", "##########################################\n", "\n", "#\n", "# We don't have to use scikit-learn to implement NN!\n", "#\n", "\n", "#\n", "# data-driven predictive model (1-nearest-neighbor)\n", "#\n", "\n", "# Python functions are first-class objects!\n", "dist = np.linalg.norm # built in to numpy... but what does norm do?\n", "\n", "num_rows, num_cols = A.shape # data size\n", "\n", "def predictiveModel( features: list[float] ) -> str:\n", " \"\"\" input: a list of four features\n", " [ sepallen, sepalwid, petallen, petalwid ]\n", " output: the predicted species of iris, from\n", " setosa (0), versicolor (1), virginica (2)\n", " \"\"\"\n", " our_features = np.asarray(features) # make a numpy array\n", "\n", " closest_flower = A[0]\n", " closest_features = A[0,0:4]\n", " closest_distance = dist(our_features - closest_features)\n", "\n", " for i in range(1, num_rows, 1):\n", " current_flower = A[i]\n", " current_features = A[i,0:4]\n", " current_distance = dist(our_features - current_features)\n", "\n", " if current_distance < closest_distance:\n", " closest_distance = current_distance # remember closest!\n", " closest_flower = current_flower\n", "\n", " # done comparing with every flower in the dataset\n", " predicted_species = int(round(closest_flower[4])) # what type is closest_flower?\n", " name = species_names[predicted_species]\n", " return f\"{name} ({predicted_species})\"\n", "\n", "#\n", "# Try it!\n", "#\n", "# features = eval(input(\"Enter new features: \"))\n", "#\n", "features = [ 4.6, 3.6, 3.0, 1.2 ]\n", "result = predictiveModel( features )\n", "print(f\"I predict {result} from features {features}\")" ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "id": "9nhDlGbRKPSB" }, "outputs": [], "source": [ "##########################################\n", "# BLOCK 19: COMMENTS ON SCIKIT-LEARN kNN\n", "##########################################\n", "\n", "#\n", "# but, we don't have to write our own ... because\n", "#\n", "# we want knn for any k (not just k=1 as above)\n", "# we want an already-debugged algorithm!\n", "# we want to ask iris-related questions instead of implementation ones...\n", "#" ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "id": "64kSAiCvKPSB" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+++ Start of data definitions +++\n", "\n", "X_all (just features) is \n", " [[4.9 3. 1.4 0.2]\n", " [4.7 3.2 1.3 0.2]\n", " [4.6 3.1 1.5 0.2]\n", " [5. 3.6 1.4 0.2]\n", " [5.4 3.9 1.7 0.4]\n", " [4.6 3.4 1.4 0.3]\n", " [4.4 2.9 1.4 0.2]\n", " [4.9 3.1 1.5 0.1]\n", " [5.4 3.7 1.5 0.2]\n", " [4.8 3.4 1.6 0.2]\n", " [4.8 3. 1.4 0.1]\n", " [4.3 3. 1.1 0.1]\n", " [5.8 4. 1.2 0.2]\n", " [5.7 4.4 1.5 0.4]\n", " [5.4 3.9 1.3 0.4]\n", " [5.1 3.5 1.4 0.3]\n", " [5.7 3.8 1.7 0.3]\n", " [5.1 3.8 1.5 0.3]\n", " [5.4 3.4 1.7 0.2]\n", " [5.1 3.7 1.5 0.4]\n", " [4.6 3.6 1. 0.2]\n", " [5.1 3.3 1.7 0.5]\n", " [4.8 3.4 1.9 0.2]\n", " [5. 3.4 1.6 0.4]\n", " [5.2 3.5 1.5 0.2]\n", " [5.2 3.4 1.4 0.2]\n", " [4.7 3.2 1.6 0.2]\n", " [4.8 3.1 1.6 0.2]\n", " [5.4 3.4 1.5 0.4]\n", " [5.2 4.1 1.5 0.1]\n", " [5.5 4.2 1.4 0.2]\n", " [4.9 3.1 1.5 0.2]\n", " [5. 3.2 1.2 0.2]\n", " [5.5 3.5 1.3 0.2]\n", " [4.9 3.6 1.4 0.1]\n", " [4.4 3. 1.3 0.2]\n", " [5.1 3.4 1.5 0.2]\n", " [5. 3.5 1.3 0.3]\n", " [4.5 2.3 1.3 0.3]\n", " [4.4 3.2 1.3 0.2]\n", " [5. 3.5 1.6 0.6]\n", " [5.1 3.8 1.9 0.4]\n", " [4.8 3. 1.4 0.3]\n", " [5.1 3.8 1.6 0.2]\n", " [4.6 3.2 1.4 0.2]\n", " [5.3 3.7 1.5 0.2]\n", " [5. 3.3 1.4 0.2]\n", " [7. 3.2 4.7 1.4]\n", " [6.4 3.2 4.5 1.5]\n", " [6.9 3.1 4.9 1.5]\n", " [5.5 2.3 4. 1.3]\n", " [6.5 2.8 4.6 1.5]\n", " [5.7 2.8 4.5 1.3]\n", " [6.3 3.3 4.7 1.6]\n", " [4.9 2.4 3.3 1. ]\n", " [6.6 2.9 4.6 1.3]\n", " [5.2 2.7 3.9 1.4]\n", " [5. 2. 3.5 1. ]\n", " [5.9 3. 4.2 1.5]\n", " [6. 2.2 4. 1. ]\n", " [6.1 2.9 4.7 1.4]\n", " [5.6 2.9 3.6 1.3]\n", " [6.7 3.1 4.4 1.4]\n", " [5.6 3. 4.5 1.5]\n", " [5.8 2.7 4.1 1. ]\n", " [6.2 2.2 4.5 1.5]\n", " [5.6 2.5 3.9 1.1]\n", " [5.9 3.2 4.8 1.8]\n", " [6.1 2.8 4. 1.3]\n", " [6.3 2.5 4.9 1.5]\n", " [6.1 2.8 4.7 1.2]\n", " [6.4 2.9 4.3 1.3]\n", " [6.6 3. 4.4 1.4]\n", " [6.8 2.8 4.8 1.4]\n", " [6.7 3. 5. 1.7]\n", " [6. 2.9 4.5 1.5]\n", " [5.7 2.6 3.5 1. ]\n", " [5.5 2.4 3.8 1.1]\n", " [5.5 2.4 3.7 1. ]\n", " [5.8 2.7 3.9 1.2]\n", " [6. 2.7 5.1 1.6]\n", " [5.4 3. 4.5 1.5]\n", " [6. 3.4 4.5 1.6]\n", " [6.7 3.1 4.7 1.5]\n", " [6.3 2.3 4.4 1.3]\n", " [5.6 3. 4.1 1.3]\n", " [5.5 2.5 4. 1.3]\n", " [5.5 2.6 4.4 1.2]\n", " [6.1 3. 4.6 1.4]\n", " [5.8 2.6 4. 1.2]\n", " [5. 2.3 3.3 1. ]\n", " [5.6 2.7 4.2 1.3]\n", " [5.7 3. 4.2 1.2]\n", " [5.7 2.9 4.2 1.3]\n", " [6.2 2.9 4.3 1.3]\n", " [5.1 2.5 3. 1.1]\n", " [5.7 2.8 4.1 1.3]\n", " [6.3 3.3 6. 2.5]\n", " [5.8 2.7 5.1 1.9]\n", " [7.1 3. 5.9 2.1]\n", " [6.3 2.9 5.6 1.8]\n", " [6.5 3. 5.8 2.2]\n", " [7.6 3. 6.6 2.1]\n", " [4.9 2.5 4.5 1.7]\n", " [7.3 2.9 6.3 1.8]\n", " [6.7 2.5 5.8 1.8]\n", " [7.2 3.6 6.1 2.5]\n", " [6.5 3.2 5.1 2. ]\n", " [6.4 2.7 5.3 1.9]\n", " [6.8 3. 5.5 2.1]\n", " [5.7 2.5 5. 2. ]\n", " [5.8 2.8 5.1 2.4]\n", " [6.4 3.2 5.3 2.3]\n", " [6.5 3. 5.5 1.8]\n", " [7.7 3.8 6.7 2.2]\n", " [7.7 2.6 6.9 2.3]\n", " [6. 2.2 5. 1.5]\n", " [6.9 3.2 5.7 2.3]\n", " [5.6 2.8 4.9 2. ]\n", " [7.7 2.8 6.7 2. ]\n", " [6.3 2.7 4.9 1.8]\n", " [6.7 3.3 5.7 2.1]\n", " [7.2 3.2 6. 1.8]\n", " [6.2 2.8 4.8 1.8]\n", " [6.1 3. 4.9 1.8]\n", " [6.4 2.8 5.6 2.1]\n", " [7.2 3. 5.8 1.6]\n", " [7.4 2.8 6.1 1.9]\n", " [7.9 3.8 6.4 2. ]\n", " [6.4 2.8 5.6 2.2]\n", " [6.3 2.8 5.1 1.5]\n", " [6.1 2.6 5.6 1.4]\n", " [7.7 3. 6.1 2.3]\n", " [6.3 3.4 5.6 2.4]\n", " [6.4 3.1 5.5 1.8]\n", " [6. 3. 4.8 1.8]\n", " [6.9 3.1 5.4 2.1]\n", " [6.7 3.1 5.6 2.4]\n", " [6.9 3.1 5.1 2.3]\n", " [5.8 2.7 5.1 1.9]\n", " [6.8 3.2 5.9 2.3]\n", " [6.7 3.3 5.7 2.5]\n", " [6.7 3. 5.2 2.3]\n", " [6.3 2.5 5. 1.9]\n", " [6.5 3. 5.2 2. ]]\n", "y_all (just labels) is \n", " [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.\n", " 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.\n", " 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.\n", " 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.\n", " 1. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2.\n", " 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2. 2.\n", " 2.]\n" ] } ], "source": [ "################################################\n", "# BLOCK 20: DEFINIING FEATURES & LABELS FOR kNN\n", "################################################\n", "\n", "print(\"+++ Start of data definitions +++\\n\")\n", "\n", "X_all = A[:,0:4] # X (features) ... is all rows, columns 0, 1, 2, 3\n", "y_all = A[:,4] # y (labels) ... is all rows, column 4 only\n", " # (look back at slide 20)\n", "print(f\"X_all (just features) is \\n {X_all}\")\n", "print(f\"y_all (just labels) is \\n {y_all}\")" ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "id": "2ChEk2CDKPSC" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Weighting sepallen by 1.0\n", "Weighting sepalwid by 1.0\n", "Weighting petallen by 1.0\n", "Weighting petalwid by 1.0\n" ] } ], "source": [ "################################################\n", "# BLOCK 21: REWEIGHTING FEATURES\n", "################################################\n", "\n", "#\n", "# we can re-weight different features here...\n", "#\n", "\n", "col_weights = { # could be called feature weight...\n", " 'sepallen':1.0,\n", " 'sepalwid':1.0,\n", " 'petallen':1.0,\n", " 'petalwid':1.0,\n", "}\n", "\n", "for col_name in col_weights:\n", " i = feature_name_to_index[col_name] # get the column index, i, of the column name\n", " weight = col_weights[col_name] # from the dictionary above\n", " print(f\"Weighting {col_name} by {weight}\")\n", " # weighting == \"multiplying\"\n", " X_all[:,i] *= weight # multiply by the weight to give this column (\"feature\")" ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "id": "sTDi2mFnKPSC" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[6. 2.9 4.5 1.5]\n", " [6.2 2.9 4.3 1.3]\n", " [6.4 2.9 4.3 1.3]\n", " [6.3 2.3 4.4 1.3]\n", " [6.2 2.8 4.8 1.8]\n", " [4.9 3.1 1.5 0.1]\n", " [6.6 3. 4.4 1.4]\n", " [7.2 3.2 6. 1.8]\n", " [6.7 3.3 5.7 2.5]\n", " [6.8 3.2 5.9 2.3]\n", " [5.1 3.4 1.5 0.2]\n", " [4.8 3.4 1.6 0.2]\n", " [4.9 3.1 1.5 0.2]\n", " [4.9 3. 1.4 0.2]\n", " [5.7 4.4 1.5 0.4]\n", " [6.7 3.1 4.7 1.5]\n", " [4.5 2.3 1.3 0.3]\n", " [5.2 3.5 1.5 0.2]\n", " [5.4 3. 4.5 1.5]\n", " [5.1 3.8 1.9 0.4]\n", " [6.7 3.3 5.7 2.1]\n", " [6.1 3. 4.9 1.8]\n", " [6.6 2.9 4.6 1.3]\n", " [6.5 3.2 5.1 2. ]\n", " [7.4 2.8 6.1 1.9]\n", " [5.4 3.4 1.7 0.2]\n", " [5.8 2.7 5.1 1.9]\n", " [5.7 2.8 4.5 1.3]\n", " [6.9 3.1 5.4 2.1]\n", " [6.4 3.2 5.3 2.3]\n", " [6.5 3. 5.8 2.2]\n", " [5. 2. 3.5 1. ]\n", " [5.1 3.8 1.6 0.2]\n", " [5.6 3. 4.1 1.3]\n", " [7.7 3. 6.1 2.3]\n", " [6.1 2.6 5.6 1.4]\n", " [4.6 3.2 1.4 0.2]\n", " [7.2 3.6 6.1 2.5]\n", " [4.6 3.4 1.4 0.3]\n", " [4.8 3.1 1.6 0.2]\n", " [7.7 2.6 6.9 2.3]\n", " [5.6 2.5 3.9 1.1]\n", " [5.5 2.4 3.8 1.1]\n", " [6.5 3. 5.2 2. ]\n", " [6.7 3. 5.2 2.3]\n", " [6.1 3. 4.6 1.4]\n", " [5.6 3. 4.5 1.5]\n", " [6.3 2.5 4.9 1.5]\n", " [5.5 2.4 3.7 1. ]\n", " [5.8 2.6 4. 1.2]\n", " [6.3 3.3 6. 2.5]\n", " [6. 2.2 4. 1. ]\n", " [5.7 2.9 4.2 1.3]\n", " [5.4 3.7 1.5 0.2]\n", " [4.4 3. 1.3 0.2]\n", " [4.6 3.1 1.5 0.2]\n", " [5. 3.5 1.3 0.3]\n", " [7.7 2.8 6.7 2. ]\n", " [5.9 3.2 4.8 1.8]\n", " [5.2 3.4 1.4 0.2]\n", " [4.4 2.9 1.4 0.2]\n", " [5.7 2.6 3.5 1. ]\n", " [4.9 2.4 3.3 1. ]\n", " [7.2 3. 5.8 1.6]\n", " [5. 3.4 1.6 0.4]\n", " [5.1 2.5 3. 1.1]\n", " [5.4 3.9 1.3 0.4]\n", " [6.7 2.5 5.8 1.8]\n", " [5.6 2.7 4.2 1.3]\n", " [6.5 3. 5.5 1.8]\n", " [6.2 2.2 4.5 1.5]\n", " [6.4 3.1 5.5 1.8]\n", " [6. 3. 4.8 1.8]\n", " [5. 2.3 3.3 1. ]\n", " [6.3 2.7 4.9 1.8]\n", " [6.5 2.8 4.6 1.5]\n", " [6.9 3.1 5.1 2.3]\n", " [5.8 2.7 3.9 1.2]\n", " [4.7 3.2 1.6 0.2]\n", " [5.1 3.7 1.5 0.4]\n", " [4.9 3.6 1.4 0.1]\n", " [5.7 2.8 4.1 1.3]\n", " [5. 3.6 1.4 0.2]\n", " [6.1 2.9 4.7 1.4]\n", " [5.8 4. 1.2 0.2]\n", " [5. 3.5 1.6 0.6]\n", " [6. 3.4 4.5 1.6]\n", " [6.9 3.1 4.9 1.5]\n", " [6.3 2.5 5. 1.9]\n", " [6.3 3.3 4.7 1.6]\n", " [4.8 3. 1.4 0.3]\n", " [6.4 3.2 4.5 1.5]\n", " [5.5 2.5 4. 1.3]\n", " [4.8 3. 1.4 0.1]\n", " [6.1 2.8 4. 1.3]\n", " [5.2 2.7 3.9 1.4]\n", " [5.1 3.5 1.4 0.3]\n", " [5.1 3.3 1.7 0.5]\n", " [6.9 3.2 5.7 2.3]\n", " [5.6 2.8 4.9 2. ]\n", " [4.4 3.2 1.3 0.2]\n", " [6.8 3. 5.5 2.1]\n", " [5.5 4.2 1.4 0.2]\n", " [5.8 2.7 5.1 1.9]\n", " [4.8 3.4 1.9 0.2]\n", " [6.3 3.4 5.6 2.4]\n", " [5.6 2.9 3.6 1.3]\n", " [5.4 3.9 1.7 0.4]\n", " [6.7 3. 5. 1.7]\n", " [4.3 3. 1.1 0.1]\n", " [5.5 3.5 1.3 0.2]\n", " [7.7 3.8 6.7 2.2]\n", " [7.3 2.9 6.3 1.8]\n", " [5.4 3.4 1.5 0.4]\n", " [5.7 2.5 5. 2. ]\n", " [5.1 3.8 1.5 0.3]\n", " [5.7 3.8 1.7 0.3]\n", " [6.4 2.7 5.3 1.9]\n", " [7.1 3. 5.9 2.1]\n", " [6. 2.7 5.1 1.6]\n", " [5.3 3.7 1.5 0.2]\n", " [5.8 2.8 5.1 2.4]\n", " [5. 3.3 1.4 0.2]\n", " [5. 3.2 1.2 0.2]\n", " [5.2 4.1 1.5 0.1]\n", " [6.4 2.8 5.6 2.2]\n", " [4.9 2.5 4.5 1.7]\n", " [6.1 2.8 4.7 1.2]\n", " [5.5 2.6 4.4 1.2]\n", " [5.5 2.3 4. 1.3]\n", " [6.7 3.1 5.6 2.4]\n", " [4.6 3.6 1. 0.2]\n", " [7.9 3.8 6.4 2. ]\n", " [7. 3.2 4.7 1.4]\n", " [6.3 2.8 5.1 1.5]\n", " [6.4 2.8 5.6 2.1]\n", " [6.8 2.8 4.8 1.4]\n", " [7.6 3. 6.6 2.1]\n", " [5.9 3. 4.2 1.5]\n", " [5.8 2.7 4.1 1. ]\n", " [6.3 2.9 5.6 1.8]\n", " [6.7 3.1 4.4 1.4]\n", " [5.7 3. 4.2 1.2]\n", " [4.7 3.2 1.3 0.2]\n", " [6. 2.2 5. 1.5]]\n", "[1. 1. 1. 1. 2. 0. 1. 2. 2. 2. 0. 0. 0. 0. 0. 1. 0. 0. 1. 0. 2. 2. 1. 2.\n", " 2. 0. 2. 1. 2. 2. 2. 1. 0. 1. 2. 2. 0. 2. 0. 0. 2. 1. 1. 2. 2. 1. 1. 1.\n", " 1. 1. 2. 1. 1. 0. 0. 0. 0. 2. 1. 0. 0. 1. 1. 2. 0. 1. 0. 2. 1. 2. 1. 2.\n", " 2. 1. 2. 1. 2. 1. 0. 0. 0. 1. 0. 1. 0. 0. 1. 1. 2. 1. 0. 1. 1. 0. 1. 1.\n", " 0. 0. 2. 2. 0. 2. 0. 2. 0. 2. 1. 0. 1. 0. 0. 2. 2. 0. 2. 0. 0. 2. 2. 1.\n", " 0. 2. 0. 0. 0. 2. 2. 1. 1. 1. 2. 0. 2. 1. 2. 2. 1. 2. 1. 1. 2. 1. 1. 0.\n", " 2.]\n" ] } ], "source": [ "################################################\n", "# BLOCK 22: PERMUTING THE DATA\n", "################################################\n", "\n", "#\n", "#\n", "# we scramble the data, to give a different TRAIN/TEST split each time...\n", "#\n", "indices = np.random.permutation(len(y_all)) # indices is a permutation-list\n", "\n", "# we scramble both X and y, necessarily with the same permutation\n", "X_labeled = X_all[indices] # we apply the _same_ permutation to each!\n", "y_labeled = y_all[indices] # again...\n", "print(X_labeled) # note that X_labeled and y_labeled are permuted identically\n", "print(y_labeled)" ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "id": "_pD7IZR1KPSC" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "total rows: 145; training with 116 rows; testing with 29 rows\n", "\t(sanity check: 116 + 29 = 145)\n" ] } ], "source": [ "################################################\n", "# BLOCK 23: DEFINING TRAIN VS TEST SETS\n", "################################################\n", "\n", "#\n", "# We next separate into test data and training data ...\n", "# + We will train on the training data...\n", "# + We will _not_ look at the testing data when building the model\n", "#\n", "# Then, afterward, we will test on the testing data -- and see how well we do!\n", "#\n", "\n", "#\n", "# a common convention: train on 80%, test on 20% Let's define the TEST_PERCENT\n", "#\n", "num_rows = X_labeled.shape[0] # the number of labeled rows\n", "test_percent = 0.20\n", "test_size = int(test_percent * num_rows) # no harm in rounding down\n", "\n", "X_test = X_labeled[:test_size] # first section are for testing\n", "y_test = y_labeled[:test_size]\n", "\n", "X_train = X_labeled[test_size:] # all the rest are for training\n", "y_train = y_labeled[test_size:]\n", "\n", "num_train_rows = len(y_train)\n", "num_test_rows = len(y_test)\n", "print(f\"total rows: {num_rows}; training with {num_train_rows} rows; testing with {num_test_rows} rows\" )\n", "print(f\"\\t(sanity check: {num_train_rows} + {num_test_rows} = {num_train_rows + num_test_rows})\")" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "id": "jvJMYC3eKPSD" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Created and trained a knn classifier with k = 84\n" ] } ], "source": [ "#######################################################\n", "# BLOCK 24: FIRST ATTEMPT TO BUILD & TRAIN A kNN MODEL\n", "#######################################################\n", "\n", "#\n", "# +++ This is the \"Model-building and Model-training Cell\"\n", "#\n", "# Create a kNN model and train it!\n", "#\n", "from sklearn.neighbors import KNeighborsClassifier\n", "\n", "k = 84 # we don't know what k to use, so we guess for now! (this will _not_ be a good value)\n", "knn_model = KNeighborsClassifier(n_neighbors = k) # here, k is the \"k\" in kNN\n", "\n", "# we train the model (it's one line!)\n", "knn_model.fit(X_train, y_train) # yay! trained!\n", "print(\"Created and trained a knn classifier with k =\", k)" ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "id": "d3cB0xryKPSD" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Predicted labels: [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.\n", " 1. 1. 1. 1. 1.]\n", "Actual labels : [1. 1. 1. 1. 2. 0. 1. 2. 2. 2. 0. 0. 0. 0. 0. 1. 0. 0. 1. 0. 2. 2. 1. 2.\n", " 2. 0. 2. 1. 2.]\n", "\n", "Results on test set: 9 correct out of 29 total.\n" ] } ], "source": [ "################################################\n", "# BLOCK 25: TEST THE kNN MODEL\n", "################################################\n", "\n", "#\n", "# +++ This is the \"Model-testing Cell\"\n", "#\n", "# Now, let's see how well we did on our \"held-out data\" (the testing data)\n", "#\n", "\n", "# We run our test set!\n", "predicted_labels = knn_model.predict(X_test)\n", "actual_labels = y_test\n", "\n", "# Let's print them so we can compare...\n", "print(\"Predicted labels:\", predicted_labels)\n", "print(\"Actual labels :\", actual_labels)\n", "\n", "# And, some overall results\n", "num_correct = sum(predicted_labels == actual_labels)\n", "total = len(actual_labels)\n", "print(f\"\\nResults on test set: {num_correct} correct out of {total} total.\")" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "id": "liXBT5-IKPSD" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "row 0 : versicolor versicolor \n", "row 1 : versicolor versicolor \n", "row 2 : versicolor versicolor \n", "row 3 : versicolor versicolor \n", "row 4 : versicolor virginica incorrect\n", "row 5 : versicolor setosa incorrect\n", "row 6 : versicolor versicolor \n", "row 7 : versicolor virginica incorrect\n", "row 8 : versicolor virginica incorrect\n", "row 9 : versicolor virginica incorrect\n", "row 10 : versicolor setosa incorrect\n", "row 11 : versicolor setosa incorrect\n", "row 12 : versicolor setosa incorrect\n", "row 13 : versicolor setosa incorrect\n", "row 14 : versicolor setosa incorrect\n", "row 15 : versicolor versicolor \n", "row 16 : versicolor setosa incorrect\n", "row 17 : versicolor setosa incorrect\n", "row 18 : versicolor versicolor \n", "row 19 : versicolor setosa incorrect\n", "row 20 : versicolor virginica incorrect\n", "row 21 : versicolor virginica incorrect\n", "row 22 : versicolor versicolor \n", "row 23 : versicolor virginica incorrect\n", "row 24 : versicolor virginica incorrect\n", "row 25 : versicolor setosa incorrect\n", "row 26 : versicolor virginica incorrect\n", "row 27 : versicolor versicolor \n", "row 28 : versicolor virginica incorrect\n", "\n", "Correct: 9 out of 29\n" ] }, { "data": { "text/plain": [ "9" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "################################################\n", "# BLOCK 26: PRETTY-PRINT PREDICTED LABELS\n", "################################################\n", "\n", "#\n", "# Let's print these more helpfully, in a vertical table\n", "#\n", "\n", "def compareLabels(predicted_labels: np.ndarray, actual_labels: np.ndarray) -> int:\n", " ''' a more neatly formatted comparison, returning the number correct '''\n", " num_labels = len(predicted_labels)\n", " num_correct = 0\n", "\n", " for i in range(num_labels):\n", " predicted = int(round(predicted_labels[i])) # round-to-int protects from float imprecision\n", " actual = int(round(actual_labels[i]))\n", " result = \"incorrect\"\n", " if predicted == actual: # if they match,\n", " result = \"\" # no longer incorrect\n", " num_correct += 1 # and we count a match!\n", "\n", " # note the justification formatting:\n", " # :>3d right justifies integers (d) to width 3\n", " # :<12s left justifies strings (s) to width 12\n", " print(f\"row {i:>3d} : \", end = \"\")\n", " print(f\"{species_names[predicted]:>12s} \", end = \"\")\n", " print(f\"{species_names[actual]:<12s} {result}\")\n", "\n", " print()\n", " print(f\"Correct: {num_correct} out of {num_labels}\")\n", " return num_correct\n", "\n", "#\n", "# let's try it out!\n", "#\n", "\n", "compareLabels(predicted_labels,actual_labels)" ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "id": "h_ZigXq2KPSE" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "predicted_species = [1.]\n", "I predict versicolor (1) from features [6.7, 3.3, 5.7, 2.1]\n" ] } ], "source": [ "################################################\n", "# BLOCK 27: USE THE FIRST-ATTEMPT kNN MODEL\n", "################################################\n", "\n", "#\n", "# Ok! We have our knn model, we could just use it...\n", "#\n", "\n", "#\n", "# data-driven predictive model (k-nearest-neighbor), using scikit-learn\n", "#\n", "\n", "def predictiveModel( features: list[float] ) -> str:\n", " ''' input: a list of four features\n", " [ sepallen, sepalwid, petallen, petalwid ]\n", " output: the predicted species of iris, from\n", " setosa (0), versicolor (1), virginica (2)\n", " '''\n", " our_features = np.asarray([features]) # extra brackets needed\n", " predicted_species = knn_model.predict(our_features)\n", " print(f\"predicted_species = {predicted_species}\")\n", "\n", " predicted_species = int(round(predicted_species[0])) # unpack one element\n", " name = species_names[predicted_species]\n", " return f\"{name} ({predicted_species})\"\n", "\n", "#\n", "# Try it!\n", "#\n", "# features = eval(input(\"Enter new features: \"))\n", "#\n", "features = [6.7,3.3,5.7,2.1] # [5.8,2.7,4.1,1.0] [4.6,3.6,3.0,2.2] [6.7,3.3,5.7,2.1]\n", "result = predictiveModel( features )\n", "print(f\"I predict {result} from features {features}\")" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "id": "kPByHBBnKPSE" }, "outputs": [], "source": [ "################################################\n", "# BLOCK 28: COMMENTS ON CHOICE OF BEST k\n", "################################################\n", "\n", "#\n", "# Except, we didn't really explore whether this was the BEST model we could build!\n", "#\n", "#\n", "# We used k = 84 (a neighborhood size of 84 flowers)\n", "# In a dataset of only 140ish flowers, with three species, this seems like a bad idea!\n", "#\n", "# Perhaps we should try ALL the neighborhood sizes in their own TRAIN/TEST split\n", "# and see which neighborhood size works the best, for irises, at least...\n", "#" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "id": "ZUmotr7hKPSE" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "k: 1 cv accuracy: 0.9482\n", "k: 2 cv accuracy: 0.9225\n", "k: 3 cv accuracy: 0.9482\n", "k: 4 cv accuracy: 0.9308\n", "k: 5 cv accuracy: 0.9395\n", "k: 6 cv accuracy: 0.9308\n", "k: 7 cv accuracy: 0.9482\n", "k: 8 cv accuracy: 0.9478\n", "k: 9 cv accuracy: 0.9478\n", "k: 10 cv accuracy: 0.9391\n", "k: 11 cv accuracy: 0.9478\n", "k: 12 cv accuracy: 0.9478\n", "k: 13 cv accuracy: 0.9478\n", "k: 14 cv accuracy: 0.9478\n", "k: 15 cv accuracy: 0.9482\n", "k: 16 cv accuracy: 0.9304\n", "k: 17 cv accuracy: 0.9391\n", "k: 18 cv accuracy: 0.9304\n", "k: 19 cv accuracy: 0.9217\n", "k: 20 cv accuracy: 0.9304\n", "k: 21 cv accuracy: 0.9130\n", "k: 22 cv accuracy: 0.9304\n", "k: 23 cv accuracy: 0.9304\n", "k: 24 cv accuracy: 0.9304\n", "k: 25 cv accuracy: 0.9478\n", "k: 26 cv accuracy: 0.9391\n", "k: 27 cv accuracy: 0.9478\n", "k: 28 cv accuracy: 0.9565\n", "k: 29 cv accuracy: 0.9304\n", "k: 30 cv accuracy: 0.9217\n", "k: 31 cv accuracy: 0.9304\n", "k: 32 cv accuracy: 0.9217\n", "k: 33 cv accuracy: 0.9217\n", "k: 34 cv accuracy: 0.9217\n", "k: 35 cv accuracy: 0.9304\n", "k: 36 cv accuracy: 0.9217\n", "k: 37 cv accuracy: 0.9304\n", "k: 38 cv accuracy: 0.9217\n", "k: 39 cv accuracy: 0.9217\n", "k: 40 cv accuracy: 0.9304\n", "k: 41 cv accuracy: 0.9304\n", "k: 42 cv accuracy: 0.9130\n", "k: 43 cv accuracy: 0.9043\n", "k: 44 cv accuracy: 0.9130\n", "k: 45 cv accuracy: 0.8957\n", "k: 46 cv accuracy: 0.8870\n", "k: 47 cv accuracy: 0.8957\n", "k: 48 cv accuracy: 0.8957\n", "k: 49 cv accuracy: 0.8957\n", "k: 50 cv accuracy: 0.9043\n", "k: 51 cv accuracy: 0.9043\n", "k: 52 cv accuracy: 0.8703\n", "k: 53 cv accuracy: 0.8873\n", "k: 54 cv accuracy: 0.8964\n", "k: 55 cv accuracy: 0.9047\n", "k: 56 cv accuracy: 0.8964\n", "k: 57 cv accuracy: 0.8877\n", "k: 58 cv accuracy: 0.8877\n", "k: 59 cv accuracy: 0.8873\n", "k: 60 cv accuracy: 0.7333\n", "k: 61 cv accuracy: 0.7076\n", "k: 62 cv accuracy: 0.5518\n", "k: 63 cv accuracy: 0.4565\n", "k: 64 cv accuracy: 0.4304\n", "k: 65 cv accuracy: 0.3967\n", "k: 66 cv accuracy: 0.3623\n", "k: 67 cv accuracy: 0.3536\n", "k: 68 cv accuracy: 0.3449\n", "k: 69 cv accuracy: 0.3536\n", "k: 70 cv accuracy: 0.3536\n", "k: 71 cv accuracy: 0.3536\n", "k: 72 cv accuracy: 0.3536\n", "k: 73 cv accuracy: 0.3536\n", "k: 74 cv accuracy: 0.3536\n", "k: 75 cv accuracy: 0.3536\n", "k: 76 cv accuracy: 0.3536\n", "k: 77 cv accuracy: 0.3536\n", "k: 78 cv accuracy: 0.3536\n", "k: 79 cv accuracy: 0.3536\n", "k: 80 cv accuracy: 0.3536\n", "k: 81 cv accuracy: 0.3536\n", "k: 82 cv accuracy: 0.3536\n", "k: 83 cv accuracy: 0.3536\n", "k: 84 cv accuracy: 0.3536\n", "best_k = 28 yields the highest average cv accuracy.\n" ] }, { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAA04AAAIjCAYAAAA0vUuxAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAeltJREFUeJzt3Xd4VNXaxuFn0ntICEkIhN5bgiC9KgoWEEUBRUGwHQGliB49qNhjOSCIIOonliMIUgRsKNIERJDeewk9hEAS0jOzvz9IRmICyYSEmWR+93XNdciePXveCRtPnqy13mUyDMMQAAAAAOCKXOxdAAAAAAA4OoITAAAAABSC4AQAAAAAhSA4AQAAAEAhCE4AAAAAUAiCEwAAAAAUguAEAAAAAIUgOAEAAABAIQhOAAAAAFAIghMAXAevvPKKTCaT4uPj7V0KyihHuoeOHDkik8mkL774wt6lAMB1Q3ACAAfy8MMPy2QyqVmzZjIMI9/zJpNJw4cPt36d+wOsyWTSvHnz8p1fnB+2p06dKpPJpNatW1/DJ4Gj6NWrl3x8fJScnHzFcwYMGCAPDw+dO3fuutYGAGUJwQkAHND27ds1f/58m17z2muvFRi2bDVjxgzVqFFD69ev14EDB675erCvAQMGKC0tTd99912Bz6empmrhwoXq0aOHKlaseN3rA4CyguAEAA7G29tb9erVsykIRUdHa9u2bVf84bioDh8+rD/++EMTJkxQpUqVNGPGjGu6XmlKSUmxdwllQq9eveTv76+ZM2cW+PzChQuVkpKiAQMGXPfaAKAsITgBgJ0cPXpUderUUZMmTXTmzBnrcRcXF7344os2BaH+/fvbHLYKMmPGDAUFBemOO+7Qvffee8XgdOHCBY0aNUo1atSQp6enqlatqoEDB+aZEpienq5XXnlF9erVk5eXlypXrqx77rlHBw8elCStWLFCJpNJK1asyHPtgtbPPPzww/Lz89PBgwd1++23y9/f3/qD/qpVq3TfffepWrVq8vT0VGRkpEaNGqW0tLR8de/Zs0d9+/ZVpUqV5O3trfr162vs2LGSpOXLl8tkMhX4PZ85c6ZMJpPWrl1b4Pdjw4YNMplM+vLLL/M998svv8hkMumHH36QJCUnJ2vkyJHW711oaKhuueUWbdq06Qp/K1d2pXvoct7e3rrnnnu0dOlSxcXFFfjZ/P391atXLyUkJGjMmDFq2rSp/Pz8FBAQoNtuu01bt24ttJYuXbqoS5cu+Y4//PDDqlGjRp5jFotFEydOVOPGjeXl5aWwsDA98cQTOn/+vE2fHwCuJ4ITANjBwYMH1alTJ/n7+2vFihUKCwvL8/wDDzygunXrFjkIubq66sUXX9TWrVuvadRpxowZuueee+Th4aH7779f+/fv119//ZXnnIsXL6pjx46aPHmybr31Vk2aNEn/+te/tGfPHh0/flySZDabdeedd+rVV19VixYtNH78eI0YMUKJiYnasWNHsWrLzs5W9+7dFRoaqv/+97/q06ePJGnOnDlKTU3Vk08+qcmTJ6t79+6aPHmyBg4cmOf127ZtU+vWrbVs2TI99thjmjRpknr37q3vv/9eyvnBPzIyssCwOGPGDNWuXVtt27YtsLaWLVuqVq1a+vbbb/M9N3v2bAUFBal79+6SpH/961/66KOP1KdPH02dOlVjxoyRt7e3du/ebdP3o7B76HIDBgxQdnZ2vvoSEhL0yy+/6O6775a3t7cOHTqkBQsW6M4779SECRP07LPPavv27ercubNOnjxpU31X88QTT+jZZ59V+/btNWnSJA0ePFgzZsxQ9+7dlZWVVWLvAwAlygAAlLpx48YZkoyzZ88au3fvNiIiIowbb7zRSEhIyHPeoEGDDF9fX8MwDOPLL780JBnz58+3Pi/JGDZsmPXrw4cPG5KM9957z8jOzjbq1q1rREVFGRaLJd/7FmbDhg2GJGPJkiWGYRiGxWIxqlataowYMSLPeS+//HK+unLlvu/06dMNScaECROueM7y5csNScby5cvzPJ/7mT7//PM83xdJxvPPP5/veqmpqfmOxcTEGCaTyTh69Kj1WKdOnQx/f/88xy6vxzAM44UXXjA8PT2NCxcuWI/FxcUZbm5uxrhx4/K9z+VeeOEFw93dPc/faUZGhlGhQgVjyJAh1mOBgYF5/g6Lqqj3UEGys7ONypUrG23bts1zfNq0aYYk45dffjEMwzDS09MNs9mc55zDhw8bnp6exmuvvZbn2D//jjp37mx07tw533sPGjTIqF69uvXrVatWGZKMGTNm5Dlv8eLFBR4HAEfBiBMAXEc7duxQ586dVaNGDf32228KCgq64rkDBgwo9qjTggULbK5txowZCgsLU9euXaWcDn79+vXTrFmzZDabrefNmzdPUVFRuvvuu/Ndw2QyWc8JCQnRU089dcVziuPJJ5/Md8zb29v655SUFMXHx6tdu3YyDEObN2+WJJ09e1a///67hgwZomrVql2xnoEDByojI0Nz5861Hps9e7ays7P14IMPXrW2fv36KSsrK09Tj19//VUXLlxQv379rMcqVKigdevWFXsEx5Z7KJerq6v69++vtWvX6siRI9bjM2fOVFhYmG6++WZJkqenp1xcLv1oYDabde7cOfn5+al+/frFmkpYkDlz5igwMFC33HKL4uPjrY8WLVrIz89Py5cvL5H3AYCSRnACgOuoZ8+e8vf31y+//KKAgICrnpsbhLZs2VLkIDRgwADVqVPH5rVOZrNZs2bNUteuXXX48GEdOHBABw4cUOvWrXXmzBktXbrUeu7BgwfVpEmTq17v4MGDql+/vtzc3IpcQ2Hc3NxUtWrVfMdjY2P18MMPKzg4WH5+fqpUqZI6d+4sSUpMTJQkHTp0SJIKrbtBgwa68cYb80zXmzFjhtq0aaM6depc9bVRUVFq0KCBZs+ebT02e/ZshYSE6KabbrIee/fdd7Vjxw5FRkaqVatWeuWVV6z1FYUt99DlcteE5TaJOH78uFatWqX+/fvL1dVVyll79P7776tu3bry9PRUSEiIKlWqpG3btlm/l9dq//79SkxMVGhoqCpVqpTncfHixQLXYQGAIyA4AcB11KdPHx08eLDI3epsDUKXh62FCxcWua5ly5bp1KlTmjVrlurWrWt99O3bV8oJDyXtSiNPl49uXe7y0ZDLz73lllv0448/6t///rcWLFigJUuWWBtLWCwWm+saOHCgVq5cqePHj+vgwYP6888/Cx1tytWvXz8tX75c8fHxysjI0KJFi9SnT588AbJv3746dOiQJk+erIiICL333ntq3Lixfv755yK9h633UK4WLVqoQYMG+uabbyRJ33zzjQzDyNNN76233tLo0aPVqVMnff311/rll1+0ZMkSNW7cuNDvZVH/Pi0Wi0JDQ7VkyZICH6+99ppNnwsArpeS+1UgAKBQ7733ntzc3DR06FD5+/vrgQceuOr5uUHo4YcfLnIQevDBB/XGG2/o1VdfVa9evYr0mhkzZig0NFRTpkzJ99z8+fP13Xffadq0afL29lbt2rULbfBQu3ZtrVu3TllZWXJ3dy/wnNwpZhcuXMhz/OjRo0WqWTn7Xe3bt09ffvllnmYQS5YsyXNerVq1pJxpboXp37+/Ro8erW+++UZpaWlyd3fPM9Xuavr166dXX31V8+bNU1hYmJKSktS/f/9851WuXFlDhw7V0KFDFRcXpxtuuEFvvvmmbrvttkLfw9Z76HIDBgzQSy+9pG3btmnmzJmqW7eubrzxRuvzc+fOVdeuXfXZZ5/led2FCxcUEhJy1WsHBQUVOHL2z7/P2rVr67ffflP79u3zTLMEAEfHiBMAXEcmk0mffPKJ7r33Xg0aNEiLFi0q9DUPPvig6tSpo1dffbVI73H5qFNRrp+Wlqb58+frzjvv1L333pvvMXz4cCUnJ1uv1adPnyt278sdFevTp4/i4+P14YcfXvGc6tWry9XVVb///nue56dOnVqkz5n7WS+/Zu6fJ02alOe8SpUqqVOnTpo+fbpiY2MLrCdXSEiIbrvtNn399deaMWOGevToUWhoyNWwYUM1bdpUs2fP1uzZs1W5cmV16tTJ+rzZbM435S00NFQRERHKyMgo0nsU5x7KlTu69PLLL2vLli359m5ydXXN9/2YM2eOTpw4Uei1a9eurT179ujs2bPWY1u3btWaNWvynNe3b1+ZzWa9/vrr+a6RnZ2dL0gDgKNgxAkArjMXFxd9/fXX6t27t/r27auffvopzxqYf3J1ddXYsWM1ePDgIr/HgAED9Prrr2vLli2Fnrto0SIlJydfcXSqTZs21s1w+/Xrp2effVZz587VfffdpyFDhqhFixZKSEjQokWLNG3aNEVFRWngwIH66quvNHr0aK1fv14dO3ZUSkqKfvvtNw0dOlR33XWXAgMDdd9992ny5MkymUyqXbu2fvjhB5vWuDRo0EC1a9fWmDFjdOLECQUEBGjevHkF7gf0wQcfqEOHDrrhhhv0+OOPq2bNmjpy5Ih+/PHHfN+ngQMH6t5775WkAn/Av5p+/frp5ZdflpeXlx555JE80wuTk5NVtWpV3XvvvYqKipKfn59+++03/fXXXxo/fnyR38PWeyhXzZo11a5dO+vo5T+D05133qnXXntNgwcPVrt27bR9+3bNmDHDOmJ3NUOGDNGECRPUvXt3PfLII4qLi9O0adPUuHFjJSUlWc/r3LmznnjiCcXExGjLli269dZb5e7urv3792vOnDmaNGmS9XsPAA7F3m39AMAZFNQWPDU11ejcubPh5+dn/Pnnn4bxj3bkl8vKyjJq16591Xbk//T5558bkgptR96zZ0/Dy8vLSElJueI5Dz/8sOHu7m7Ex8cbhmEY586dM4YPH25UqVLF8PDwMKpWrWoMGjTI+nzu5xs7dqxRs2ZNw93d3QgPDzfuvfde4+DBg9Zzzp49a/Tp08fw8fExgoKCjCeeeMLYsWNHge3IC/q+GIZh7Nq1y+jWrZvh5+dnhISEGI899pixdevWfNcwDMPYsWOHcffddxsVKlQwvLy8jPr16xsvvfRSvmtmZGQYQUFBRmBgoJGWlnbF70tB9u/fb/2+r169Ot91n332WSMqKsrw9/c3fH19jaioKGPq1KmFXreo91BhpkyZYkgyWrVqle+59PR045lnnjEqV65seHt7G+3btzfWrl2br9V4Qe3IDcMwvv76a6NWrVqGh4eHER0dbfzyyy/52pHn+uSTT4wWLVoY3t7ehr+/v9G0aVPjueeeM06ePFmkzwEA15vJuJYt5gEAKIeys7MVERGhnj175lvvAwBwTqxxAgDgHxYsWKCzZ8/maTgBAHBujDgBAJBj3bp12rZtm15//XWFhISU2KavAICyjxEnAAByfPTRR3ryyScVGhqqr776yt7lAAAcCCNOAAAAAFAIRpwAAAAAoBAEJwAAAAAohNNtgGuxWHTy5En5+/vLZDLZuxwAAAAAdmIYhpKTkxUREZFnw/KCOF1wOnnypCIjI+1dBgAAAAAHcezYMVWtWvWq5zhdcPL395dyvjkBAQH2LgcAAACAnSQlJSkyMtKaEa7G6YJT7vS8gIAAghMAAACAIi3hoTkEAAAAABSC4AQAAAAAhSA4AQAAAEAhCE4AAAAAUAiCEwAAAAAUguAEAAAAAIUgOAEAAABAIQhOAAAAAFAIghMAAAAAFILgBAAAAACFIDgBAAAAQCEITgAAAABQCIITAAAAABSC4AQAAAAAhSA4AQAAAEAhCE4AAAAAUAiCEwDkMAxD248nKi3TbO9SAACAgyE4AYCkuOR0Df7iL/X8cLX+9fVGe5cDAAAcDMEJgNP7dedp9Zi4Siv2npUkrdx3Vptjz9u7LAAA4EAITgCcVkpGtp6ft02P/2+jElIy1ahygLrWryRJmrrioL3LAwAADsTN3gUAgD1sjj2vUbO36Mi5VJlM0uOdamn0LfV0LCFNK/at1JJdZ7T3dLLqh/vbu1QAAOAAGHEC4FSyzRZN+m2/7p22VkfOpSoi0EszH22jF25rKE83V9UJ9VOPxuGSpGkrGXUCAACXMOJkRxuOJOjD5Qc0qls9RUVWKNY1TiWmacafsXqobXWFBXiVeI1FdejsRX219qgkydPNRR5uLpf9r6v1a1cX01Wv4+/lpo51K8ndtXiZPik9S2v2xyvTbCnW622RZTaUmW1RRrY5538t1q8zsi3y9nDV8K515O/lXuq1OIPzKZk6FJ+iG6pVkMl09fvoSo6eS9HI2Vu0OfaCJOmu6Ai9dlcTBXrn/Tsa2qWOft5xWou2ntToW+opMtinRD4DAAAouwhOdjRzfaxW7D0rd1cXfTqwZbGuMfa7HVq2J06rDsRr7r/aFjtwXIvUzGw9+uUGHYpPKZHrNasaqPf7Rat2JT+bXrfmQLye+XarTiell0gdJeHUhXR9cH9ze5dR5iWnZ6n31DU6ei5VtzYKU8w9TVXRz7PIrzcMQ3M2HNcr3+9UaqZZ/l5ueqN3E90VXaXA85tWDVTHuiFatT9eH/9+UG/0blqCnwYAAJRFJsMwDHsXcT0lJSUpMDBQiYmJCggIsGstB+Iu6pb3V8owpMUjO6pBuG317DyZqDs+WG39+umb6mj0rfVLodKr+8932zVzXazCAjx1b4uqeUZfcv+ckTMSYynkdttxIkmJaVnycnfR2Dsa6cHW1QodXcjINuu/v+zVp6sOS5KqVPBWjZDSHyFwc8k/spb7Z5Ok/1t9WGaLoUn9o6/4AzqK5plvt2repuPWr0P8PPXefc3UtX5ooa9NSMnUC/O36ZedZyRJrWsGa0K/aFWp4H3V1609eE73f/qnPNxctPrfXRXqb78RXQAAUDpsyQaMONlRnVA/3dYkXD9tP62pyw/aPDIxdfml9Re1Kvnq0NkUfbj8gDrWq6QbawSXUsX5/brztGaui5XJJL3fN1rt6oRc0/VOJ6ZrzJytWn0gXi8t2KHle+L0Tp9mquRf8OjCntNJGjlri/acTpYkDWhdTWPvaCgfD/vf2t4erpr42369tGCHbqwRrIhCflBHwb7felLzNh2Xi0l6o3dTfb7msPbHXdTgz//SwLbV9cJtDeXt4Vrga1fuO6sxc7bqbHKG3F1NeubW+nqsY61Cp4xKUptawWperYI2x17Q9NVH9PxtDUrh0wEAgLKC5hB2NrRLHUnSD9tO6ogNU90OxF3UTztOSZKmDrhBfW6oKoshjZy1RUnpWaVW7+XiktL173nbJEmPd6x1zaFJksIDvfTVkFZ6+c5G8nBz0bI9ceox8Xf9tutMnvMsFkOfrT6sXh+u0Z7Tyaro66HPBrXUm3c3dYjQJEnDutZRVGQFJaVn69m5W2WxONXgbok4cSFNY7/bLkka3rWOHmhdTd8/1UGD29eQJH219qh6frhaO04k5nldepZZryzaqUHT1+tscobqhPrpu6Ht9a/OtYsUmiTJZDJpWM6/z6//PKrEtOvz7woAADgmgpOdNakSqK71K8liSB/ZsG/MRysOyjCkbg3D1CA8QK/0aqTIYG+duJCmlxfsKNWalRNcnpmzVedTs9Q4IkCjb61XYtd2cTFpSIea+n54BzUI99e5lEw9+tUG/ee77UrNzNbpxHQNnL5er/+wS5nZFt3cIFSLR3bSzQ3DSqyGkuDu6qL3+0bJ291Vaw6c0xd/HLF3SWWK2WJo9OwtSkrPVnRkBT11c11Jkpe7q8b1bKyvhrRSqL+nDsRd1N1T1+ijFQdlthjaeTJRd05ebf1+P9yuhn54qoOaVAm0uYabGoSqfpi/LmZk639r+fsDAMCZEZwcwPCbLv1We/7m4zp5Ia3Q848lpGrBlhN5Xuvv5a6J/ZrL1cWkBVtOasHmE0V+/+3HE/Xmj7t0IC65yK/5/I8jWrU/Xp5uLprUP1qebgVPlboW9cP9tXB4ez3eqZYkaea6WN02aZW6T/xdqw/Ey8vdRW/e3UT/N6jlFafy2VutSn4ae0dDSdLbi/do/5mif4/PJmdo+Z44ZV+HDoGO6OPfD2rd4QT5eLhqYr/ofI1POtWrpF9GdlKPxuHKMht6Z/Ee3Tl5tXpPWaMDcRdVyd9TXwy+Ua/0aiwv9+Ldny4uJj3ZpbYkafqaI0rLNJfIZwMAAGUPwckBtKgerDa1gpVlNvTJ74cKPf/j3y/9Zr1DnRBFX9bGvEX1ID2VE6ReWrBDxxJSr3ods8XQh8v26+6pa/TpqsO644PV+mrtERXWL2T3qSS98/MeSdKLdzZSndDS2yDU081V/7m9oWY+2lrhAV46ei5ViWlZalY1UD8+3VEDWlcvdmvq62VA62rqUr+SMrMtGjFrizKzCw9CP247pW4TVmrwF3+p3yd/Kvbc1f8uy5ttxy9owq/7JEmv9GqsGiG+BZ4X5Ouhjx68Qe/e20y+Hq7afSpJWWZDtzYK0y8jO6lLEZpHFObOZpUVGeythJRMzfor9pqvBwAAyiaCk4MY3vXSNKRv1sfqbHLGFc+LS0rXtxsudRcb1rVOAdepoxuqVVByRrZGzd5yxdGKYwmp6vfxWv33133KthiKDPZWRrZFLy/cqYc//0txyQW39E7PMmvErM3KNF+aIvdg62rF/MS2aVcnRL+M7KQh7WvquR71Ne/Jdja3K7cXk8mkd/s0U5CPu3adStLE3/Zd8dzk9CyN/naLhs3cZF1Ts/Hoed026XfN2XCs0FBbHqRmZmvErC3Kthi6vWm47mtR9arnm0wm9W0ZqZ9HdFK/lpEaf1+UPn6ohYJ9PUqkHjdXFz3R6dKo06e/HypS8AUAAOUPwclBtK9TUVGRFZSRbdFnqw9f8bxPV136wa1F9SC1qZW/e56bq4sm9msuP083bTh6Pt+6KcMwNHfjcd02aZU2HD0vP083Tegbpd+f7apXezWWp5uLVu47qx4TV+mXnafzXf/tn/do35mLCvHz1Dv3Nruuoz2BPu56uWcjDe1Sxy77VV2L0AAvxdxzaS+gaSsP6q8jCfnO+etIgm6btErzN52Qi0l66qY6Wj6mi1rVCFZKplnPzt2mYTM36XxKph0+wfXz+g+7dDg+ReEBXnrr7qZFvseqVfTRO/c2U58WVUv8vry3RVVV8vfUycR0LdxS9GmwAACg/ChbP32WYyaTScO7XtbBKzV/B6+ElEx9/eelqULDu9a54g+H1Sr66LW7GkuSJi7dry3HLkiSzqdkatjMTRozZ6suZmTrxhpB+nlER91zw6UfNAflLKJvVDlACSmZeuJ/G/X8vG1KyciWJC3fG2ddcP/f+5opxIYNSCH1aFJZ97a41P1w9LdblJzT/TDLbNF/f9mrfh+v1fHzaYoM9ta3T7TVM7fWV80QX33zeBs916O+3FxM+mn7afWY9LtW7T9r749TKhbvOK1v1h+TySRN6BelCj4lM2p0rbzcXfVoh5qSpI9WXpoqCwAAnAvByYHc3CBUDcIvdfAqqAPb52sOKy3LrMYRAepSv9JVr3V38yrqGRUhs8XQiFmb9evOSz9w/7T9tNxcTHq2e33NerytIoPzbhRbN8xfC4ZdattsMkmz/jqm2z9YpaW7z+jZOZdajz/crkaJrB1xRuN6NlKVCt46lpCm13/YpYNnL6rPR3/ow+UHZDGkPjdU1U9Pd1TLy/bicnUxaWiXOvpuaHvVquSrM0kZeuiz9Xrt+11Kzyo/zQrOJKXr+fk57e071VK72tfe3r4kDWhTXQFebjp0NkW/FjAaCwAAyjeT4QyLJi5jy+7A9rBo60k9/c1mVfBx15p/3yRfz0t7EiWlZ6n928uUnJ6tqQNu0O1NKxd6rcS0LN0+aZVOXNapr1YlX03q11xNqxbemvnPQ+f0zLdb87y+ftilTnfF7VIGad2hc+r/6Z8yDMnDzUWZ2RYFervrrbub6o5mV/97Tcs0662fdut/fx6VJNUL89PEfs3VKKJ497LFYmjVgXjVC/NT5cDibdBrGIZ+3XXmqmvzimLRlpNafyRBTaoEaP6T7eXh5ni/1xn/615NXnZATasEatHw9sWeEnggLlnHz6epc71Kdm1usud0kuKTM9W+TsVi17HzZKISU7NKZB+3K0nNzNYvO08rJePqvyioH+5/XTcABwCUfbZkA4KTgzFbDHWbsFKH41P0n9sb6PGcRelTlh/Qe7/sVe1KvloyqrNciriJ5+U/pD/Uprr+c3tDeXsUPfQkpmVp3MIdWrDlpDxcXbToqfZqEO5437eyJubn3fp45aUOih3qhOi/90UpPNCryK9fvidOz87dqviLmfJwddGz3evrkQ41i3xfSNLpxHSNmbNVqw/EKyzAU4tHdFJQMRoqfLX2iF5euNPm1xXEy91FPzzVUXVCHbPxR0JKptq/vUxpWWZ1qV9J797bTKH+Rf97M1sMTVt5UO8vudSUpVvDUL3d5/pPe80yWzR52QF9uGy/LIZ0R9PKevPuJjZNjczMtmjS0n36aMVBWQypV1SEXu/dRIHe7iVa6+bY8xo1e4uOFKGzpItJmvV4W7WqSXgCABQNwekqHD04SdK3fx3Tc/O2qZK/p1Y911WGIXV4Z5nOpWRq/H1R6lNIl7F/2hR7XiZJzasFFbumPw+dk7+XmxpH2L6JKPLLyDZr8tIDiqjgrf43RtoUeHLFX8zQ8/O267fdZyRJ7WpX1Pi+UUUaOfpp+ym9MH+7tXOfJHVvHKZpD7awaeRh35lk9Zy8WhnZFrWtVVEB3m42f45cLiaT+txQVd0aOdZGxv80b+NxvfDddmVmWxTs66G372mqWxuHF/q6YwmpGv3tFv115LwkyWSSDEMK8fPQu/c2000Nrs/nPhyfopGzt2hrztrH3DrCA7w0vm+U2hdh5OhA3EWNmr1F208k5rlGRKCXxveNVtvaFa+5zmyzRVOWH9QHy/bLbDEUHuClqMgr//fn+Pk07TyZpCoVvPXTiI4lHuAAAOUTwekqykJwysy2qMt7y3UyMV2v39VYWWZDr/2wS1WDvLV8TJcy11EOpccwDM3665he+36X0rLMCvBy01v3NNWdzSIKPD85PUuvLNqleZsutbRvWiVQj3eqpdHfbrm0iWyfpup3Y9FazGdkm3XXh2u053SyOterpC8G3+jwe2qVlH1nkjVi1hbtPpUkSbq/VaRevKORdWrt5QzD0IItJ/Tygp1KzsiWr4erXr2riRpHBGjkrC3am7Mp8oNtqmns7Y1sGhG2RUH3yht3N1X1YB+Nmr1Fh+JTJEmPdqipMd3rFzgd1zAMfb0uVm/+uEvpWRZV8Lk0xTQ80EujZm/R0XOpMpmkxzvW0uhb6xV7Y+yj51I0avYWbYq9FO7ubFZZb/ZuqkCfK4ehixnZun3SKsUmpOqu6AhN6t+8WO8NAHAuBKerKAvBSZK+/OOIxi3aqSoVvGW2GDqdlK43ejfRg22q27s0OKDD8SkaOWuzth6/NAJwT/MqeuWuxgrw+vsHzQ1HEjTq2y06lpAmF5M0tEsdPX1zXXm4uejjlQcV8/Meebu76senO6hWEfbIeuOHXfq/1YdV0ddDP4/saNOUtfIgI9us8b/u06erDskwpBoVffR+v+g8I7uJqVkau2C7fth2SsrZpPr9vtGqVvFSU5b0LLPe+2WvdQsCW9Yg2uLcxQw9P3+7luy6NDrZttal0cmICpdGJ1Mzs/Xmj7s1Y92lrp0Nwv01sX90nmm5Z5Mz9O9527RsT5wkqWPdEL13799TTFMysvX6D7s0669jkqRGlQM0qX+06oYVfYNswzA0Z+Nxvbpop1IyzfL3dNPrvZvoruiIIoXyTbHndd+0tTJbDE3sF63ezavY9H0CADgfgtNVlJXglJ5lVod3liv+4qUF96H+nvr9ua40ZcAVZZktmrx0v7VDX5UK3jk/yFfQB0v3a0rO8apBl45fvojeYjH04Gfr9MfBc2pWNVDznmx31ZHNVfvP6qHP1kuSPhvUUjc3dOzpdaXpjwPxembOVp1KTJeri0lP31RXw7rW1vrDCdbjbi4mjexWV//qXFtuBXxfV++P1zNztuhMUobcXEwadUs9/atzbbkWYwrnPy3fG6dn52xT/MWMQtfDLd19Rs/N3aZzKZfWzj3Xo76GtK+pZXvi9O95OcfdXPR8jwZ6uF2NAq/xy87Ten7eNp1PzZKnm4teuK2BBrWrUWjwOZ+SqRfmb9finI6FrWoGa0LfKFUN8rnq6/7pg6X7NWHJPvl7uumnER3zdQ4FAOByBKerKCvBSTkbpb798x5J0ot3NNSjHWvZuySUARuPJmjk7EsjSyaTVKOirw7nTMO654YqeqVX3pGoXKcS09Rj4iolpmVpWNfaerZ7gwKvn5CSqR4Tf1dccoYealNdr/duUuqfydH9c2SpVoivdepbrRBfvd8vWlGRFa56jQupmfrPd9v10/ac4FAjWD2aFL526mr2nk7W7A2XRoCK2oHxbHKGnp+3TUtzRpZqVfLVobOXPkuDcH9N6t9c9cOvPooUl5SuZ+du08p9l/Yb61g3RF2vsoVBptmi6asPKy45Q+6uJo2+pb4e71SrWMEx22xR/0/+1Iaj59WyepBmPd6mwLAKAIAITldXloLTxYxs9Zj4u1xMJi0e2VE+HsVfeA/ncjEjW68u2qk5Gy+tZQr0dtebdze54tqnXD9vP6UnZ2y6tIfXY23UulbeRf6GYejx/23Ukl1nVLuSr354qmOprckpawzD0MItJ/XSgh1Kztk0ekDrahp7R8Mi/9s1DEPzN53QuEU7dTHnGiVhcPsa+nePBkUesTYMQzPXx+r1Hy6tZVLO3lrP2LBuyTAM/e/Po3rzx93KyLYU6TW1K/lqUv/malLl2qYqHktI1W2TVuliRrZG31JPT99c95quBwAovwhOV1GWgpNy9u0xmcQUPRTLkl1ntPbgOT3WqWaR92l6ds5Wzdl4XBGBXvp5ZKc83cm+WR+rF+Zvl7urSd8NbX/NP+CWR8fPp+qz1YfVqW4ldW1QvI2ijyWk6pPfD+Xpelgcbq4m3dO8qjrULd4eSwfPXtT/1h7VrY3Dir0h8f4zyZq+5ohSCgmCtSv56fFOtUosiH+3+bhGzd4qVxeT5v6r7TV1FQUAlF8Ep6soa8EJuN4uZmTrjg9W6ei5VPWMitAH/aNlMpl08OxF3fnBaqVlmfPsMQY4IsMwNGLWFi3aelLVK/rox6c7yq+ArocAAOdmSzZg4jeAPPw83TSxX7RcXUz6futJLdhyQpnZFo2ctUVpWWa1q11Rj3ZgvR0cm8lk0uu9m6hKBW8dPZeqVxeVzCbNAADnRXACkE/zakEambMu5KUFO/XC/O3afiJRgd7umtA3ulgb9gLXW6C3u97vFy0XkzRn43H9mNO8AwCA4iA4ASjQ0K51dGONIF3MyLZumPv2PU2t+/YAZUGrmsEa2qWOJOmF+dt0/Hxqsa917mKG5m08XqKNOwAAZQfBCUCBXF1MmtA3Wv4560L6tYzUbU0r27sswGYjutVVVNVAJaVnq9eHa/RLzl5Rtli+J07dJ67SM3O26smvN8picarlwQAAmkPQHAIozLpD57T6QLye7FKblvgos46fT9XjX23UrlNJkqT+N0bqpTsbybeQhhFpmWa99dNu/e/Po3mOs7ceAJQPdNW7CoITADinjGyzJizZp09+PyTDkKpX9NH7/aJ1wxVale84kagRszbrYM4GwIPb11DVIB+9/sMuebi6aMGw9oVuKAwAcGx01QMA4B883Vz1wm0NNfPRNooI9NLRc6m6b9paTfxtn7LNf2/Sa7YYmrrigHpPWaODZ1MU6u+pr4a00riejTWkfQ11axiqTLNFI2ZtVnqW2a6fCQBw/TDiBABwOolpWXp54Q4t3HJSkhQdWcHahv+Zb7dq/ZEESVKPxuGKuaepgnw9rK89dzFD3SeuUvzFDA1qW12v3tXEbp8DAHBtmKp3FQQnAECuhVtO6MUFO5Scni0fD1e5mkxKzsiWr4erXunVWPe2qCqTKX/7/RV74/Tw539Jkj5/+EZ1bRBqh+qL78SFNH2/9aQysy1XPa9NrYpqVTP4utX1T3FJ6ZqwZJ/CArz0ZJfa8nJ3tVstAMqnMhWcpkyZovfee0+nT59WVFSUJk+erFatWhV4blZWlmJiYvTll1/qxIkTql+/vt555x316NGjyO9HcAIAXO7EhTSNnr1F6w5fGmVqUT1I7/eNVrWKPld93avf79Tna44oxM9DP4/opEr+ntep4uIzDEMLtpzQywt2KrkIbdW93V21buzNCvByvy715TIMQ4u2ntTLC3cqMS1LklQn1E/j74tSVGSF61oLgPKtzASn2bNna+DAgZo2bZpat26tiRMnas6cOdq7d69CQ/P/9u7f//63vv76a3366adq0KCBfvnlF40ePVp//PGHmjdvXqT3JDgBAP7JbDH07YZjyjZbdH+ranJzLXwJcHqWWXd9uEZ7zySra/1Kmv7wjQWOTjmKxNQsjV2wXT/kbAQcVTVQjasEXvH8ZbvjdDopXe/2aaa+N0ZetzrjL2Zo7Hfb9cvOM5KkRpUDFJecofiLGXJ1MelfnWvp6ZvrytON0ScA167MBKfWrVvrxhtv1IcffihJslgsioyM1FNPPaXnn38+3/kREREaO3ashg0bZj3Wp08feXt76+uvvy7SexKcAAAlZc/pJPX6cI0ysy167a7GGti2hr1LKtAfB+L1zJytOpWYLlcXk0beXFdPdql91YA4ZfkBvffLXrWpFaxZj7e9LnX+tP2UXlywQwkpmXJzMempm+pqaNfaupierXGLdmrR1ktr0hqE++u/90WpyVWCHwAURZnoqpeZmamNGzeqW7dufxfj4qJu3bpp7dq1Bb4mIyNDXl5eeY55e3tr9erVV3yfjIwMJSUl5XkAAFASGoQH6IXbGkiS3vxxt/adSbZ3SXlkZJv15o+79MD/rdOpxHTVDPHVvCfb6amb6xY6qnZXdIQk6c9DCTpxIa1U6zyfkqnhMzdp6IxNSkjJVINwfy0c3l4jutWVu6uLgnw99MH9zfXRgBsU7OuhPaeT1XvKGk38bZ+yzFdfpwUAJcVuwSk+Pl5ms1lhYWF5joeFhen06YJ3de/evbsmTJig/fv3y2KxaMmSJZo/f75OnTp1xfeJiYlRYGCg9REZef2mGwAAyr+H29VQ53qVlJFt0dPfbFZGtmO0KN97Oll3fbhGn646LEl6oHU1/fh0B0UXcY1Q1SAftc5pDLFg84lSq/PXnad1y/u/64dtp+TqYtLwrnW0aHgHNY7IP5p0W9PK+nVUJ93WJFzZFkMTf9uv3lPWaM9pfikKoPTZbareyZMnVaVKFf3xxx9q2/bvKQDPPfecVq5cqXXr1uV7zdmzZ/XYY4/p+++/l8lkUu3atdWtWzdNnz5daWkF/zYsIyNDGRkZ1q+TkpIUGRnJVD0AQImJS05Xj4mrlJCSqYfb1dC4no2Ktd7JMAx9v+2U9l5jEEjJMGvm+lhlZltU0ddD7/Rppm6Nworwyrxm/xWrf8/brjqhfloyqlOJruFKzczWK4t26tsNxyUbmz/8s3mEu6tJY26tr8c61pKLi+OuMwPgeGyZqud23ar6h5CQELm6uurMmTN5jp85c0bh4eEFvqZSpUpasGCB0tPTde7cOUVEROj5559XrVq1rvg+np6e8vR0/E5HAICyK9TfS+/2aaZHv9qgL/44ouPnU/V2n2YK8Sv6///EJafr2TnbtHLf2RKr66YGoXqnT7Nid/y7rWllvbxwpw7EXdTOk0kltqZo18kkPfXNJh08myKTSXq8Yy2NuqVekduNm0wm3RVdRW1rVdR/vtuu33bHKebnPVq+N04T+kYrooJ3idQJAJeze3OIVq1aafLkyVJOc4hq1app+PDhBTaH+KesrCw1bNhQffv21VtvvVWk96Q5BACgtHz5xxG9+eNuZZotCvHz0Lv3NtNNDQof6fl152k9P3+7ElIy5eHmoj43VLnmrnHNq1VQr6iIax4lGjZzk37cdkpD2tfUyz0bXdO1DMPQ138e1es/7lZmtkVhAZ56v1+02tUOuaZrfrvhmF79fpdSM83y93LTm3c3Va+oiGuqFYBzKDNd9WbPnq1Bgwbp448/VqtWrTRx4kR9++232rNnj8LCwjRw4EBVqVJFMTExkqR169bpxIkTio6O1okTJ/TKK6/o8OHD2rRpkypUKNqcbYITAKA07T6VpJGztmhvTqOIB9tU09jbG8nbI38QSsnI1us/7NKsv45JkhpWDtCk/tGqF+Z/3eu+kqW7z+iRLzcoxM9Tf75wU5FatRfkQmqm/j1vm7XN+E0NQvXevc1U0YZRuas5Ep+ikbO3aMuxC5Kk3tERevWuJgr0vr57UAEoW8rEVD1J6tevn86ePauXX35Zp0+fVnR0tBYvXmxtGBEbGysXl7//A52enq4XX3xRhw4dkp+fn26//Xb973//K3JoAgCgtDWsHKCFw9vrvV/26rPVh/X1n7H64+A5TerXXE2r/j3VbXPseY2avUVHzqVemq7WqZZG31LP4fYn6lSvkoJ9PRR/MUOrDsSra/38+ywWZsORBD39zWadTEyXu6tJz9/WUEPa1yjRNVM1Qnw1919tNXnZAX24/IAWbDmpv46c1/i+UWpTq2KJvQ8A52XXESd7YMQJAHC9rNp/VmPmbNWZpAy5uZg06pZ6erRjTU1bcUgfLNsvs8VQRKCXxveNVtvajvvD/biFO/Tl2qPqFRWhD+4v2obzytlYeOryA5q49NJnrVHRR5PvvyFPgCwNG4+e1+hvt+hoTih9olNtDe1aW25XaRxhkkkebi5ypbkE4FTKzFQ9eyA4AQCup/MpmfrPd9v1845LW20EeLkpKT1bktQrKkKv93b86WRbjl1Q7ylr5OXuog0v3iI/z8InrKRnmfXYVxu0an+8JOme5lX0Wu8mRXptSbiYka3Xv9+l2RuO2fQ6VxeTPFxd5Onukvd/3Vx1f+tqeqhN9VKrGcD1VyY2wAUAwBkE+Xpo6oAb9N/7ouTr4aqk9Gz5e7lpUv9ofXB/c4cPTZIUVTVQtUJ8lZ5l0c/br7x34uXe/nmPVu2Pl4+Hq8bfF6UJ/aKvW2iSJD9PN71zbzN9/FALm7oKmi2G0rLMupCapbjkDB1LSNPBsynadSpJk5fuL9WaATg2u65xAgDAGZhMJt3boqpa1wzWT9tP6c6oCFUpQy2zTSaT7m5eReOX7NN3m0/ovpZX30z+t11n9MUfRyRJUwbcUKx1USWle+NwdWsYpsxsy1XPsxiGMrMtyjRblJFlUabZrPSsS1+fSUzXkzM2KSElU4ZhlOjaLABlB8EJAIDrJDLYR090rm3vMoqld05wWnvonE4lpqlyYMHB70xSup6du1WS9EiHmnYNTblcXUwFdjX8J98rDEylZ5klSdkWQ0np2WVilBBAyWOqHgAAKFRksI9a1QiWYUgLt5ws8ByzxdDIWVt0PjVLjSMC9FyP+te9ztLg5e4q35zgdT4l097lALATghMAACiSu2+oIkn6btMJFdRbatrKg1p76Jx8PFw1+f7mDtda/VoE+3lIks4RnACnRXACAABFcnvTyvJwc9HeM8nadSopz3ObYs9rwpJ9kqRXezVWrUp+dqqydAT7XApOjDgBzovgBAAAiiTQ213dGl5as/TdphPW40npWRoxa7PMFkM9oyJ0b4uqdqyydAT7XgpOCQQnwGkRnAAAQJH1jr40XW/h1pPKNltkGIbGfrdDxxLSVDXIW2/e3aRcdp0L8mWqHuDs6KoHAACKrEv9UAX5uOtscobWHDynuKR0fb/1pFxdTPrg/uYK8CqfHecq5gSn86kEJ8BZMeIEAACKzMPNRXc2i5AkTV1+QOMW7ZQkjb6lnm6oFmTn6kpPcE6v8nMXCU6AsyI4AQAAm+R211t3OEGpmWa1rVVR/yqj+1MVVbDvpZE0RpwA50VwAgAANmkeWUE1Q3wlSUE+7nq/X7RcXcrfuqbLWUecWOMEOC2CEwAAsInJZNLwrnVUOdBLE/s3V3igl71LKnXWESeCE+C0aA4BAABs1qdFVfUph23HryR3xIl25IDzYsQJAACgELkb4F7MyFZGttne5QCwA4ITAABAIQK83azruM6nZNm7HAB2QHACAAAohMlkUlDOqBPT9QDnRHACAAAogtxNcAlOgHMiOAEAABRBUE5nvXMpGfYuBYAdEJwAAACKoGJOZz1akgPOieAEAABQBMFM1QOcGsEJAACgCIJyg1MqwQlwRgQnAACAIqA5BODcCE4AAABFEERwApwawQkAAKAIGHECnBvBCQAAoAj+3gA3y96lALADghMAAEARVPS7FJzOp2bKYjHsXQ6A64zgBAAAUAQVfC5tgGu2GEpKZ9QJcDYEJwAAgCLwdHOVv6ebxDonwCkRnAAAAIoo2I8GEYCzIjgBAAAU0d8NIghOgLMhOAEAABQRLckB50VwAgAAKCLrJripBCfA2RCcAAAAisg64nSR4AQ4G4ITAABAETHiBDgvghMAAEARBbPGCXBaBCcAAIAiCs7pqnee4AQ4HYITAABAEeXu43SO4AQ4HYITAABAEdGOHHBeBCcAAIAiym0OkZppVnqW2d7lALiOCE4AAABF5O/pJndXk8SoE+B0CE4AAABFZDKZFOTDdD3AGRGcAAAAbEBLcsA5EZwAAABskBuczrMJLuBUCE4AAAA2yA1O5y4SnABnQnACAACwASNOgHMiOAEAANjAOuLEGifAqRCcAAAAbGAdcSI4AU6F4AQAAGADRpwA50RwAgAAsAHtyAHnRHACAACwAVP1AOdEcAIAALDB5V31LBbD3uUAuE4ITgAAADYI8rkUnCyGlJiWZe9yAFwnBCcAAAAbuLu6KMDLTaJBBOBUCE4AAAA2YhNcwPkQnAAAAGxkbUl+keAEOAu7B6cpU6aoRo0a8vLyUuvWrbV+/fqrnj9x4kTVr19f3t7eioyM1KhRo5Senn7d6gUAAGDECXA+dg1Os2fP1ujRozVu3Dht2rRJUVFR6t69u+Li4go8f+bMmXr++ec1btw47d69W5999plmz56t//znP9e9dgAA4LzYywlwPnYNThMmTNBjjz2mwYMHq1GjRpo2bZp8fHw0ffr0As//448/1L59ez3wwAOqUaOGbr31Vt1///2FjlIBAACUpGBfT4mpeoBTsVtwyszM1MaNG9WtW7e/i3FxUbdu3bR27doCX9OuXTtt3LjRGpQOHTqkn376SbfffvsV3ycjI0NJSUl5HgAAANci2NddYqoe4FTc7PXG8fHxMpvNCgsLy3M8LCxMe/bsKfA1DzzwgOLj49WhQwcZhqHs7Gz961//uupUvZiYGL366qslXj8AAHBe1hEnpuoBTsPuzSFssWLFCr311luaOnWqNm3apPnz5+vHH3/U66+/fsXXvPDCC0pMTLQ+jh07dl1rBgAA5Y91xIngBDgNu404hYSEyNXVVWfOnMlz/MyZMwoPDy/wNS+99JIeeughPfroo5Kkpk2bKiUlRY8//rjGjh0rF5f8OdDT01Oenp6l9CkAAIAzyh1xojkE4DzsNuLk4eGhFi1aaOnSpdZjFotFS5cuVdu2bQt8TWpqar5w5OrqKkkyDKOUKwYAALgk2IeueoCzsduIkySNHj1agwYNUsuWLdWqVStNnDhRKSkpGjx4sCRp4MCBqlKlimJiYiRJPXv21IQJE9S8eXO1bt1aBw4c0EsvvaSePXtaAxQAAEBpC/a7FJzSssxKyzTL24OfQ4Dyzq7BqV+/fjp79qxefvllnT59WtHR0Vq8eLG1YURsbGyeEaYXX3xRJpNJL774ok6cOKFKlSqpZ8+eevPNN+34KQAAgLPx9XCVh6uLMs0WJaRmqoqHt71LAlDKTIaTzXFLSkpSYGCgEhMTFRAQYO9yAABAGdXmraU6nZSu74d3UNOqgfYuB0Ax2JINylRXPQAAAEcR5Juzzom9nACnQHACAAAohoq5wSklw96lALgOCE4AAADFEJwTnM5dZMQJcAYEJwAAgGLIDU7nmaoHOAWCEwAAQDEE+7KXE+BMCE4AAADFEERwApwKwQkAAKAYKhKcAKdCcAIAACiGIB+CE+BMCE4AAADFUNGP4AQ4E4ITAABAMeSOOF1Iy5LZYti7HAClzObgdOjQodKpBAAAoAwJ8nGXJBmGdIGW5EC5Z3NwqlOnjrp27aqvv/5a6enppVMVAACAg3NzdVGFnPDEdD2g/LM5OG3atEnNmjXT6NGjFR4erieeeELr168vneoAAAAcWDANIgCnYXNwio6O1qRJk3Ty5ElNnz5dp06dUocOHdSkSRNNmDBBZ8+eLZ1KAQAAHAyb4ALOo9jNIdzc3HTPPfdozpw5euedd3TgwAGNGTNGkZGRGjhwoE6dOlWylQIAADgY6ya4rHECyr1iB6cNGzZo6NChqly5siZMmKAxY8bo4MGDWrJkiU6ePKm77rqrZCsFAABwMNZNcC8SnIDyzs3WF0yYMEGff/659u7dq9tvv11fffWVbr/9drm4XMpgNWvW1BdffKEaNWqURr0AAAAOgxEnwHnYHJw++ugjDRkyRA8//LAqV65c4DmhoaH67LPPSqI+AAAAh1WRNU6A07A5OO3fv7/Qczw8PDRo0KDi1gQAAFAmBNFVD3AaNq9x+vzzzzVnzpx8x+fMmaMvv/yypOoCAABweMF+BCfAWdgcnGJiYhQSEpLveGhoqN56662SqgsAAMDh5U7VO09wAso9m4NTbGysatasme949erVFRsbW1J1AQAAOLzcqXrnUjJlGIa9ywFQimwOTqGhodq2bVu+41u3blXFihVLqi4AAACHVzFnql5GtkWpmWZ7lwOgFNkcnO6//349/fTTWr58ucxms8xms5YtW6YRI0aof//+pVMlAACAA/J2d5Wn26Ufp1jnBJRvNnfVe/3113XkyBHdfPPNcnO79HKLxaKBAweyxgkAADgVk8mkir4eOpmYroSUTEUG+9i7JAClxObg5OHhodmzZ+v111/X1q1b5e3traZNm6p69eqlUyEAAIADC8oNTmyCC5RrNgenXPXq1VO9evVKthoAAIAyJjh3E9yLBCegPCtWcDp+/LgWLVqk2NhYZWbm/Y/EhAkTSqo2AAAAh5cbnM4z4gSUazYHp6VLl6pXr16qVauW9uzZoyZNmujIkSMyDEM33HBD6VQJAADgoHKD0zmaQwDlms1d9V544QWNGTNG27dvl5eXl+bNm6djx46pc+fOuu+++0qnSgAAAAcV7MMmuIAzsDk47d69WwMHDpQkubm5KS0tTX5+fnrttdf0zjvvlEaNAAAADivYjxEnwBnYHJx8fX2t65oqV66sgwcPWp+Lj48v2eoAAAAcXMXc5hAEJ6Bcs3mNU5s2bbR69Wo1bNhQt99+u5555hlt375d8+fPV5s2bUqnSgAAAAcVxFQ9wCnYHJwmTJigixcvSpJeffVVXbx4UbNnz1bdunXpqAcAAJxORabqAU7BpuBkNpt1/PhxNWvWTMqZtjdt2rTSqg0AAMDh5Y44JaZlKdtskZurzSshAJQBNv3LdnV11a233qrz58+XXkUAAABlSAUfD5lMl/58PjXL3uUAKCU2/0qkSZMmOnToUOlUAwAAUMa4uphUwdtdYhNcoFyzOTi98cYbGjNmjH744QedOnVKSUlJeR4AAADOxroJ7kWCE1Be2dwc4vbbb5ck9erVS6bccWlJhmHIZDLJbDaXbIUAAAAOLtjXQwfPpiguOd3epQAoJTYHp+XLl5dOJQAAAGVU44hA/XXkvOZuPK67oqvYuxwApcDm4NS5c+fSqQQAAKCMeqRDTf3vz6NatT9eW49dUFRkBXuXBKCE2Rycfv/996s+36lTp2upBwAAoMyJDPbRXdERmr/phD5cfkCfDmxp75IAlDCbg1OXLl3yHbt8rRNrnAAAgDMa2qWOvtt8Qkt2ndGe00lqEB5g75IAlCCbu+qdP38+zyMuLk6LFy/WjTfeqF9//bV0qgQAAHBwdUL9dFuTcEnS1OUH7V0OgBJm84hTYGBgvmO33HKLPDw8NHr0aG3cuLGkagMAAChThnapo5+2n9YP205q9C31VCPE194lASghNo84XUlYWJj27t1bUpcDAAAoc5pUCVTX+pVkMaSPVjDqBJQnNo84bdu2Lc/XhmHo1KlTevvttxUdHV2StQEAAJQ5w2+qo+V7z2r+5uMa0a2uIip427skACXA5uAUHR0tk8kkwzDyHG/Tpo2mT59ekrUBAACUOS2qB6tNrWD9eShBn/x+SK/0amzvkgCUAJuD0+HDh/N87eLiokqVKsnLy6sk6wIAACizhnetqz8PrdM362M1rGsdVfL3tHdJAK6RzcGpevXqpVMJAABAOdG+TkVFRVbQ1mMX9Nnqw3r+tgb2LgnANbK5OcTTTz+tDz74IN/xDz/8UCNHjiypugAAAMosk8mk4V3rSJK+/vOoElOz7F0SgGtkc3CaN2+e2rdvn+94u3btNHfu3JKqCwAAoEy7uUGoGoT762JGtr7444i9ywFwjWwOTufOnStwL6eAgADFx8eXVF0AAABlmouLSUNzRp0+/+OwUjKy7V0SgGtgc3CqU6eOFi9enO/4zz//rFq1apVUXQAAAGXeHU0rq2aIry6kZmnGuqP2LgfANbC5OcTo0aM1fPhwnT17VjfddJMkaenSpRo/frwmTpxYGjUCAACUSa4uJj3Zpbaem7tNn646rIFta8jL3dXeZQEoBpuD05AhQ5SRkaE333xTr7/+uiSpRo0a+uijjzRw4MDSqBEAAKDMurt5FU36bb9OXEjTnA3H9FDbGvYuCUAxmIx/7mRrg7Nnz8rb21t+fn4lW1UpSkpKUmBgoBITExUQEGDvcgAAgBP4au0RvbxwpxpWDtDPIzrauxwAOWzJBsXaADc7O1t169ZVpUqVrMf3798vd3d31ajBb1EAAAAu17ZWRUnS6cQ0e5cCoJhsbg7x8MMP648//sh3fN26dXr44YeLVcSUKVNUo0YNeXl5qXXr1lq/fv0Vz+3SpYtMJlO+xx133FGs9wYAAChtQb4ekqQLaVkyW4o92QeAHdkcnDZv3lzgPk5t2rTRli1bbC5g9uzZGj16tMaNG6dNmzYpKipK3bt3V1xcXIHnz58/X6dOnbI+duzYIVdXV9133302vzcAAMD1UMHbXZJkGNKF1Ex7lwOgGGwOTiaTScnJyfmOJyYmymw221zAhAkT9Nhjj2nw4MFq1KiRpk2bJh8fH02fPr3A84ODgxUeHm59LFmyRD4+PgQnAADgsNxcXVTB51J4Ok9wAsokm4NTp06dFBMTkyckmc1mxcTEqEOHDjZdKzMzUxs3blS3bt3+LsjFRd26ddPatWuLdI3PPvtM/fv3l6+vb4HPZ2RkKCkpKc8DAADgegv2uTRd79xFghNQFtncHOKdd95Rp06dVL9+fXXseKkrzKpVq5SUlKRly5bZdK34+HiZzWaFhYXlOR4WFqY9e/YU+vr169drx44d+uyzz654TkxMjF599VWb6gIAAChpQb4eUnwKI05AGWXziFOjRo20bds29e3bV3FxcUpOTtbAgQO1Z88eNWnSpHSqvILPPvtMTZs2VatWra54zgsvvKDExETr49ixY9e1RgAAAEkKzmkQkZCSZe9SABSDzSNOkhQREaG33nrrmt88JCRErq6uOnPmTJ7jZ86cUXh4+FVfm5KSolmzZum111676nmenp7y9PS85loBAACuRe5UvYSUDHuXAqAYihWcJCk1NVWxsbHKzMw73NysWbMiX8PDw0MtWrTQ0qVL1bt3b0mSxWLR0qVLNXz48Ku+ds6cOcrIyNCDDz5YzE8AAABw/QQx4gSUaTYHp7Nnz2rw4MH6+eefC3ze1s56o0eP1qBBg9SyZUu1atVKEydOVEpKigYPHixJGjhwoKpUqaKYmJg8r/vss8/Uu3dvVaxY0daPAAAAcN1VzAlOrHECyiabg9PIkSN14cIFrVu3Tl26dNF3332nM2fO6I033tD48eNtLqBfv346e/asXn75ZZ0+fVrR0dFavHixtWFEbGysXFzyLsXau3evVq9erV9//dXm9wMAALCH3BGncykEJ6Assjk4LVu2TAsXLlTLli3l4uKi6tWr65ZbblFAQIBiYmJ0xx132FzE8OHDrzg1b8WKFfmO1a9fX4bBrtsAAKDsCPbN2ceJ4ASUSTZ31UtJSVFoaKgkKSgoSGfPnpUkNW3aVJs2bSr5CgEAAMqBYN9LzaoSCE5AmWRzcKpfv7727t0rSYqKitLHH3+sEydOaNq0aapcuXJp1AgAAFDm/d1Vj+AElEU2T9UbMWKETp06JUkaN26cevTooRkzZsjDw0NffPFFadQIAABQ5gXlTNVLyzIrLdMsbw9Xe5cEwAY2B6fL23+3aNFCR48e1Z49e1StWjWFhISUdH0AAADlgp+nm9xdTcoyGzqfmilvD297lwTABjZP1fsnHx8f3XDDDYQmAACAqzCZTAr2ZboeUFZdc3ACAABA0QSxzgkoswhOAAAA10kwm+ACZRbBCQAA4Dphqh5QdhGcAAAArhOCE1B2Famr3rZt24p8wWbNml1LPQAAAOUWa5yAsqtIwSk6Olomk0mGYchkMl31XLPZXFK1AQAAlCsV/VjjBJRVRZqqd/jwYR06dEiHDx/WvHnzVLNmTU2dOlWbN2/W5s2bNXXqVNWuXVvz5s0r/YoBAADKqNwRp3MXCU5AWVOkEafq1atb/3zffffpgw8+0O2332491qxZM0VGRuqll15S7969S6dSAACAMo6uekDZZXNziO3bt6tmzZr5jtesWVO7du0qqboAAADKnb/XOGXZuxQANrI5ODVs2FAxMTHKzPz7NyWZmZmKiYlRw4YNS7o+AACAcuPyNU4Wi2HvcgDYoEhT9S43bdo09ezZU1WrVrV20Nu2bZtMJpO+//770qgRAACgXKjg4y5JMlsMJadnKzDnawCOz+bg1KpVKx06dEgzZszQnj17JEn9+vXTAw88IF9f39KoEQAAoFzwdHOVn6ebLmZkKyE1k+AElCE2BydJ8vX11eOPP17y1QAAAJRzwb4el4JTSoZqhvBLZ6CssHmNkyT973//U4cOHRQREaGjR49Kkt5//30tXLiwpOsDAAAoV4J8aRABlEU2B6ePPvpIo0eP1m233abz589bN7wNCgrSxIkTS6NGAACAciM4Z3re+RRakgNlic3BafLkyfr00081duxYubn9PdOvZcuW2r59e0nXBwAAUK4E+3pKks4RnIAyxebgdPjwYTVv3jzfcU9PT6WkpJRUXQAAAOVSsG/OiBOb4AJlis3BqWbNmtqyZUu+44sXL2YfJwAAgEL8vcaJ4ASUJTZ31Rs9erSGDRum9PR0GYah9evX65tvvlFMTIz+7//+r3SqBAAAKCcq5gQn1jgBZYvNwenRRx+Vt7e3XnzxRaWmpuqBBx5QRESEJk2apP79+5dOlQAAAOVEkM+l4MQaJ6BsKdY+TgMGDNCAAQOUmpqqixcvKjQ0tOQrAwAAKIeCc0ecWOMElCk2r3G66aabdOHCBUmSj4+PNTQlJSXppptuKvkKAQAAyhHWOAFlk83BacWKFcrMzP8PPT09XatWrSqpugAAAMql3DVOyenZysy22LscAEVU5Kl627Zts/55165dOn36tPVrs9msxYsXq0qVKiVfIQAAQDkS4OUuF5NkMaQLqZkKDfCyd0kAiqDIwSk6Olomk0kmk6nAKXne3t6aPHlySdcHAABQrri4mBTk46FzKZlKIDgBZUaRg9Phw4dlGIZq1aql9evXq1KlStbnPDw8FBoaKldX19KqEwAAoNwI9s0JThdZ5wSUFUUOTtWrV5ckWSzMxQUAALgW1gYRdNYDyoxitSNXzjqn2NjYfI0ievXqVRJ1AQAAlFvBPmyCC5Q1NgenQ4cO6e6779b27dtlMplkGIYkyWQySTmNIgAAAHBlwX5sgguUNTa3Ix8xYoRq1qypuLg4+fj4aOfOnfr999/VsmVLrVixonSqBAAAKEcYcQLKHptHnNauXatly5YpJCRELi4ucnFxUYcOHRQTE6Onn35amzdvLp1KAQAAyom/1zhl2bsUAEVk84iT2WyWv7+/JCkkJEQnT56UcppH7N27t+QrBAAAKGeCfd0lSQkpGfYuBUAR2Tzi1KRJE23dulU1a9ZU69at9e6778rDw0OffPKJatWqVTpVAgAAlCPBvp6SpIQURpyAssLm4PTiiy8qJSVFkvTaa6/pzjvvVMeOHVWxYkXNnj27NGoEAAAoV1jjBJQ9Ngen7t27W/9cp04d7dmzRwkJCQoKCrJ21gMAAMCVBVmn6mXKMAx+hgLKAJvXOBUkODiYf/AAAABFVDFnql6m2aKUTLZyAcqCIo043XPPPUW+4Pz586+lHgAAgHLP28NVXu4uSs+y6HxKpvw8bZ4EBOA6K9KIU2BgoPUREBCgpUuXasOGDdbnN27cqKVLlyowMLA0awUAACg3ctc5JbDOCSgTivTrjc8//9z653//+9/q27evpk2bJldXVymnRfnQoUMVEBBQepUCAACUI8F+HjqZmE5wAsoIm9c4TZ8+XWPGjLGGJklydXXV6NGjNX369JKuDwAAoFwKYsQJKFNsDk7Z2dnas2dPvuN79uyRxWIpqboAAADKtWDfnJbkqQQnoCyweSXi4MGD9cgjj+jgwYNq1aqVJGndunV6++23NXjw4NKoEQAAoNzJDU7nGHECygSbg9N///tfhYeHa/z48Tp16pQkqXLlynr22Wf1zDPPlEaNAAAA5Q6b4AJli83BycXFRc8995yee+45JSUlSRJNIQAAAGwU5MsaJ6AsuaZNAwhMAAAAxRNMcALKlCIFpxtuuEFLly5VUFCQmjdvLpPJdMVzN23aVJL1AQAAlEvW4ERzCKBMKFJwuuuuu+Tp6SlJ6t27d2nXBAAAUO5Zu+ox4gSUCUUKTuPGjSvwzwAAACie3H2cLqRlyWwx5Opy5Rk9AOzP5n2cAAAAcO2CfNwlSYYhXWC6HuDwijTiFBQUdNV1TZdLSEi41poAAADKPTdXFwV6uysxLUvnUzNV0c/T3iUBuIoiBaeJEyeWfiUAAABOJtjXQ4lpWTp3MVN1Qu1dDYCrKVJwGjRoUOlXAgAA4GSCfT10OD5F55mqBzi8a9rHKT09XZmZef+hs7cTAABA0eQ2iEhIybJ3KQAKYXNziJSUFA0fPlyhoaHy9fVVUFBQnoetpkyZoho1asjLy0utW7fW+vXrr3r+hQsXNGzYMFWuXFmenp6qV6+efvrpJ5vfFwAAwN6CfS81iEhIybB3KQAKYXNweu6557Rs2TJ99NFH8vT01P/93//p1VdfVUREhL766iubrjV79myNHj1a48aN06ZNmxQVFaXu3bsrLi6uwPMzMzN1yy236MiRI5o7d6727t2rTz/9VFWqVLH1YwAAANhdkC8jTkBZYfNUve+//15fffWVunTposGDB6tjx46qU6eOqlevrhkzZmjAgAFFvtaECRP02GOPafDgwZKkadOm6ccff9T06dP1/PPP5zt/+vTpSkhI0B9//CF390u/oalRo4atHwEAAMAhVMzdBJc1ToDDs3nEKSEhQbVq1ZJy1jPlth/v0KGDfv/99yJfJzMzUxs3blS3bt3+LsbFRd26ddPatWsLfM2iRYvUtm1bDRs2TGFhYWrSpIneeustmc3mK75PRkaGkpKS8jwAAAAcwd9rnAhOgKOzOTjVqlVLhw8fliQ1aNBA3377rZQzElWhQoUiXyc+Pl5ms1lhYWF5joeFhen06dMFvubQoUOaO3euzGazfvrpJ7300ksaP3683njjjSu+T0xMjAIDA62PyMjIItcIAABQmoJ9CU5AWWFzcBo8eLC2bt0qSXr++ec1ZcoUeXl5adSoUXr22WdLo0Yri8Wi0NBQffLJJ2rRooX69eunsWPHatq0aVd8zQsvvKDExETr49ixY6VaIwAAQFERnICyw+Y1TqNGjbL+uVu3btqzZ482btyoOnXqqFmzZkW+TkhIiFxdXXXmzJk8x8+cOaPw8PACX1O5cmW5u7vL1dXVeqxhw4Y6ffq0MjMz5eHhke81np6e8vRkJ24AAOB4glnjBJQZNo84/XPEpnr16rrnnntsCk2S5OHhoRYtWmjp0qXWYxaLRUuXLlXbtm0LfE379u114MABWSwW67F9+/apcuXKBYYmAAAAR5bbVS8106z0rCuv2QZgfzYHpxo1aqhz58769NNPdf78+Wt689GjR+vTTz/Vl19+qd27d+vJJ59USkqKtcvewIED9cILL1jPf/LJJ5WQkKARI0Zo3759+vHHH/XWW29p2LBh11QHAACAPfh7usnd1SQxXQ9weDYHpw0bNqhVq1Z67bXXVLlyZfXu3Vtz585VRobtG7f169dP//3vf/Xyyy8rOjpaW7Zs0eLFi60NI2JjY3Xq1Cnr+ZGRkfrll1/0119/qVmzZnr66ac1YsSIAluXAwAAODqTyURnPaCMMBmGYRTnhYZhaMWKFZo5c6bmzZsni8Wie+65R9OnTy/5KktQUlKSAgMDlZiYqICAAHuXAwAAnFyPib9rz+lkfTWklTrVq2TvcgCnYks2sHnEKZfJZFLXrl316aef6rffflPNmjX15ZdfFvdyAAAATokGEUDZUOzgdPz4cb377ruKjo5Wq1at5OfnpylTppRsdQAAAOVcEC3JgTLB5nbkH3/8sWbOnKk1a9aoQYMGGjBggBYuXKjq1auXToUAAADlWDBrnIAywebg9MYbb+j+++/XBx98oKioqNKpCgAAwEkw4gSUDTYHp9jYWJlMl9pmrlmzRi1btmSDWQAAgGKqyBonoEyweY1TbmiSpNtuu00nTpwo6ZoAAACcRu6I07mLBCfAkRW7OYRyWpIDAACg+HLXODHiBDi2awpOAAAAuDbB1jVOWfYuBcBVXFNw+vjjjxUWFlZy1QAAADiZy/dxsliYzQM4qmsKTg888IDMZrMWLFig3bt3l1xVAAAATiLI112SZLYYSk7Ptnc5AK7A5uDUt29fffjhh5KktLQ0tWzZUn379lWzZs00b9680qgRAACg3PJ0c5Wf56VGxwmscwIcls3B6ffff1fHjh0lSd99950Mw9CFCxf0wQcf6I033iiNGgEAAMq13FEn9nICHJfNwSkxMVHBwcGSpMWLF6tPnz7y8fHRHXfcof3795dGjQAAAOVabmc9ghPguGwOTpGRkVq7dq1SUlK0ePFi3XrrrZKk8+fPy8vLqzRqBAAAKNdy93I6T3ACHJabrS8YOXKkBgwYID8/P1WvXl1dunSRcqbwNW3atDRqBAAAKNesLclZ4wQ4LJuD09ChQ9WqVSsdO3ZMt9xyi1xcLg1a1apVizVOAAAAxcBUPcDx2RycJKlly5Zq2bKlJMlsNmv79u1q166dgoKCSro+AACAci/Il+AEODqb1ziNHDlSn332mZQTmjp37qwbbrhBkZGRWrFiRWnUCAAAUK5VZI0T4PBsDk5z585VVFSUJOn777/X4cOHtWfPHo0aNUpjx44tjRoBAADKtdwRp3MEJ8Bh2Ryc4uPjFR4eLkn66aefdN9996levXoaMmSItm/fXho1AgAAlGu5zSHO0xwCcFg2B6ewsDDt2rVLZrNZixcv1i233CJJSk1Nlaura2nUCAAAUK4Fs8YJcHg2N4cYPHiw+vbtq8qVK8tkMqlbt26SpHXr1qlBgwalUSMAAEC5lttVLzk9W5nZFnm42fy7bQClzObg9Morr6hJkyY6duyY7rvvPnl6ekqSXF1d9fzzz5dGjQAAAOVaoLe7XEySxZAupGYqNMDL3iUB+IditSO/99578x0bNGhQSdQDAADgdFxcTAry8dC5lEwlEJwAh1SsceCVK1eqZ8+eqlOnjurUqaNevXpp1apVJV8dAACAk7Du5XSRdU6AI7I5OH399dfq1q2bfHx89PTTT+vpp5+Wt7e3br75Zs2cObN0qgQAACjnctc5xdMgAnBINk/Ve/PNN/Xuu+9q1KhR1mNPP/20JkyYoNdff10PPPBASdcIAABQ7kVUuDQ978T5NHuXAqAANo84HTp0SD179sx3vFevXjp8+HBJ1QUAAOBUIoN9JEnHzqfauxQABbA5OEVGRmrp0qX5jv/222+KjIwsqboAAACcSmRQTnBKIDgBjsjmqXrPPPOMnn76aW3ZskXt2rWTJK1Zs0ZffPGFJk2aVBo1AgAAlHtVg70lSceZqgc4JJuD05NPPqnw8HCNHz9e3377rSSpYcOGmj17tu66667SqBEAAKDcq5YzVe/4+VSZLYZcXUz2LgnAZWwKTtnZ2Xrrrbc0ZMgQrV69uvSqAgAAcDKVA73l5mJSltnQmaR0RVTwtndJAC5j0xonNzc3vfvuu8rOzi69igAAAJyQq4vJGpZY5wQ4HpubQ9x8881auXJl6VQDAADgxCJz1jkdY50T4HBsXuN022236fnnn9f27dvVokUL+fr65nm+V69eJVkfAACA07jUWe8cI06AA7I5OA0dOlSSNGHChHzPmUwmmc3mkqkMAADAyVj3ciI4AQ7H5uBksVhKpxIAAAAnxya4gOOyeY0TAAAASkdkUG5zCNY4AY6myMFp2bJlatSokZKSkvI9l5iYqMaNG+v3338v6foAAACcRu6I05nkdKVnsfwBcCRFDk4TJ07UY489poCAgHzPBQYG6oknntD7779f0vUBAAA4jYq+HvJ2d5VhSCcuMOoEOJIiB6etW7eqR48eV3z+1ltv1caNG0uqLgAAAKdjMplUjQYRgEMqcnA6c+aM3N3dr/i8m5ubzp49W1J1AQAAOCX2cgIcU5GDU5UqVbRjx44rPr9t2zZVrly5pOoCAABwSlWDLo04HWfECXAoRQ5Ot99+u1566SWlp6fney4tLU3jxo3TnXfeWdL1AQAAOBVakgOOqcj7OL344ouaP3++6tWrp+HDh6t+/fqSpD179mjKlCkym80aO3ZsadYKAABQ7uWucYplxAlwKEUOTmFhYfrjjz/05JNP6oUXXpBhGFLOIsbu3btrypQpCgsLK81aAQAAyj3rGif2cgIcSpGDkyRVr15dP/30k86fP68DBw7IMAzVrVtXQUFBpVchAACAE4nMWeOUmJalpPQsBXhduTkXgOvHpuCUKygoSDfeeGPJVwMAAODkfD3dFOzroYSUTB1LSFXjiEB7lwTAluYQAAAAuD4ig5iuBzgaghMAAICDiWQTXMDhEJwAAAAcDC3JAcdDcAIAAHAwuQ0iGHECHAfBCQAAwMFYW5KfZ40T4CgITgAAAA7m8hGn3L0zAdgXwQkAAMDBRFTwlotJysi26Gxyhr3LAUBwAgAAcDwebi6qHJg7XY91ToAjIDgBAAA4oKrs5QQ4FIcITlOmTFGNGjXk5eWl1q1ba/369Vc894svvpDJZMrz8PLyuq71AgAAlDb2cgIci92D0+zZszV69GiNGzdOmzZtUlRUlLp37664uLgrviYgIECnTp2yPo4ePXpdawYAAChtuQ0iYglOgEOwe3CaMGGCHnvsMQ0ePFiNGjXStGnT5OPjo+nTp1/xNSaTSeHh4dZHWFjYda0ZAACgtFWryBonwJHYNThlZmZq48aN6tat298FubioW7duWrt27RVfd/HiRVWvXl2RkZG66667tHPnziuem5GRoaSkpDwPAAAAR/d3S3LWOAGOwK7BKT4+XmazOd+IUVhYmE6fPl3ga+rXr6/p06dr4cKF+vrrr2WxWNSuXTsdP368wPNjYmIUGBhofURGRpbKZwEAAChJuWucTiWmKctssXc5gNOz+1Q9W7Vt21YDBw5UdHS0OnfurPnz56tSpUr6+OOPCzz/hRdeUGJiovVx7Nix614zAACArSr5ecrDzUUWQzp1Id3e5QBOz82ebx4SEiJXV1edOXMmz/EzZ84oPDy8SNdwd3dX8+bNdeDAgQKf9/T0lKenZ4nUCwAAcL24uJhUNchbh86mKDYhVdUq+ti7JMCp2XXEycPDQy1atNDSpUutxywWi5YuXaq2bdsW6Rpms1nbt29X5cqVS7FSAACA669abktyGkQAdmfXESdJGj16tAYNGqSWLVuqVatWmjhxolJSUjR48GBJ0sCBA1WlShXFxMRIkl577TW1adNGderU0YULF/Tee+/p6NGjevTRR+38SQAAAErW3w0iCE6Avdk9OPXr109nz57Vyy+/rNOnTys6OlqLFy+2NoyIjY2Vi8vfA2Pnz5/XY489ptOnTysoKEgtWrTQH3/8oUaNGtnxUwAAAJS8yODcluR01gPszWQYhmHvIq6npKQkBQYGKjExUQEBAfYuBwAA4Ip+3n5KT87YpOjIClowrL29ywHKHVuyQZnrqgcAAOAscluSM1UPsD+CEwAAgIPKDU7nUjKVkpFt73IAp0ZwAgAAcFCB3u4K8Lq0JP0465wAuyI4AQAAODCm6wGOgeAEAADgwHJbkscSnAC7IjgBAAA4sGoV2QQXcAQEJwAAAAcWGZSzl1MCa5wAeyI4AQAAOLCqOWucjjPiBNgVwQkAAMCB5a5xOpaQKsMw7F0O4LQITgAAAA6sas5UvZRMsxJSMu1dDuC0CE4AAAAOzMvdVWEBnpKkY+zlBNgNwQkAAMDBXT5dD4B9EJwAAAAcnHUTXBpEAHZDcAIAAHBwtCQH7I/gBAAA4OByW5IzVQ+wH4ITAACAg6vGVD3A7ghOAAAADi53jdPJC2kyW9jLCbAHghMAAICDCw/wkrurSVlmQ6eT0u1dDuCUCE4AAAAOztXFpIgKuQ0imK4H2APBCQAAoAzI3cspluAE2AXBCQAAoAzIXed0nOAE2AXBCQAAoAyIDM6ZqneevZwAeyA4AQAAlAG5U/WOnkuxdymAUyI4AQAAlAFNqgRKkjYfu6CDZy/auxzA6RCcAAAAyoCaIb7q1jBMhiF9vPKgvcsBnA7BCQAAoIwY2rW2JGn+phM6eYG1TsD1RHACAAAoI26oFqQ2tYKVbTH06apD9i4HcCoEJwAAgDJkWNc6kqRZ64/p3MUMe5cDOA2CEwAAQBnSoU6ImlYJVFqWWV/8ccTe5QBOg+AEAABQhphMJg3tcmmt05d/HFFyepa9SwKcAsEJAACgjOneOFy1KvkqKT1bM9fF2rscwCkQnAAAAMoYFxeTnux8adTp/1YfVnqW2d4lAeUewQkAAKAMuiu6iiICvXQ2OUNzNx63dzlAuUdwAgAAKIM83Fz0WKdakqSPfz+obLPF3iUB5RrBCQAAoIzqf2M1Bft66FhCmn7cfsre5QDlGsEJAACgjPL2cNWQ9jUkSVOXH5TFYti7JKDcIjgBAACUYQ+1rSE/TzftPZOsZXvi7F0OUG4RnAAAAMqwQG93DWhTTZI0ZcUBGQajTkBpIDgBAACUcY90qCkPNxdtjr2gPw8l2LscoFwiOAEAAJRxof5e6tuyqiRp6ooD9i4HKJcITgAAAOXAE51qy9XFpFX747XjRKK9ywHKHYITAABAORAZ7KPbmoRLEhviAqWA4AQAAFBO3Nvi0nS9RVtPKjObDXGBkkRwAgAAKCc61AlRJX9PJaRkauW+s/YuByhXCE4AAADlhJuri3pHR0iS5m9iuh5QkghOAAAA5cg9N1yarrd0d5wupGbauxyg3CA4AQAAlCMNKweoYeUAZZot+mHbKXuXA5QbBCcAAIByps8NVSSm6wEliuAEAABQzvSKjpCLSdoUe0GH41PsXQ5QLhCcAAAAyplQfy91rFtJkvTd5hP2LgcoFwhOAAAA5dA9l03Xs1gMe5cDlHkEJwAAgHLo1kbh8vN00/Hzadpw9Ly9ywHKPIITAABAOeTt4arbm4ZLNIkASgTBCQAAoJzK3dPpx22nlJ5ltnc5QJlGcAIAACinWtUIVpUK3krOyNaSXWfsXQ5QphGcAAAAyikXF1OeJhEAio/gBAAAUI7d3fxScPp9f7ziktPtXQ5QZhGcAAAAyrFalfzUvFoFmS2GFm05ae9ygDLLIYLTlClTVKNGDXl5eal169Zav359kV43a9YsmUwm9e7du9RrBAAAKKvuaZ47XY/NcIHisntwmj17tkaPHq1x48Zp06ZNioqKUvfu3RUXF3fV1x05ckRjxoxRx44dr1utAAAAZdGdzSLk7mrSrlNJ2n0qyd7lAGWS3YPThAkT9Nhjj2nw4MFq1KiRpk2bJh8fH02fPv2KrzGbzRowYIBeffVV1apV67rWCwAAUNYE+XropgahkqTvNjPqBBSHXYNTZmamNm7cqG7duv1dkIuLunXrprVr117xda+99ppCQ0P1yCOPFPoeGRkZSkpKyvMAAABwNrl7On23+YSyzRZ7lwOUOXYNTvHx8TKbzQoLC8tzPCwsTKdPny7wNatXr9Znn32mTz/9tEjvERMTo8DAQOsjMjKyRGoHAAAoS7rWD1UFH3edTc7QmoPn7F0OUObYfaqeLZKTk/XQQw/p008/VUhISJFe88ILLygxMdH6OHbsWKnXCQAA4Gg83FzUKypCYk8noFjc7PnmISEhcnV11ZkzeXeyPnPmjMLDw/Odf/DgQR05ckQ9e/a0HrNYLg01u7m5ae/evapdu3ae13h6esrT07PUPgMAAEBZcXfzKvpq7VH9svO0LmZky8/Trj8KAmWKXUecPDw81KJFCy1dutR6zGKxaOnSpWrbtm2+8xs0aKDt27dry5Yt1kevXr3UtWtXbdmyhWl4AAAAVxEdWUE1Q3yVnmXRrzsLXhYBoGB2/zXD6NGjNWjQILVs2VKtWrXSxIkTlZKSosGDB0uSBg4cqCpVqigmJkZeXl5q0qRJntdXqFBBkvIdBwAAQF4mk0m9o6vo/d/26bvNJ6wNIwAUzu7BqV+/fjp79qxefvllnT59WtHR0Vq8eLG1YURsbKxcXMrUUiwAAACH1bt5hN7/bZ/WHIhXXFK6QgO87F0SUCaYDMMw7F3E9ZSUlKTAwEAlJiYqICDA3uUAAABcd/dMXaNNsRf04h0N9WhH9sSE87IlGzCUAwAA4GR6N68iSVqwhc1wgaIiOAEAADiZO5pWlpuLSTtOJOlAXLK9ywHKBIITAACAk6no56nO9SpJkhZsPmnvcoAygeAEAADghC6frmexONWSd6BYCE4AAABOqFvDMPl5uun4+TRtjD1v73IAh0dwAgAAcELeHq7q0SRckvTdZppEAIUhOAEAADipu3Om6/247ZQyss32LgdwaAQnAAAAJ9WmVkWFBXgqMS1LK/aetXc5gEMjOAEAADgpVxeT7orOaRLBdD3gqghOAAAATqx3TnBaujtOiWlZ9i4HcFgEJwAAACfWsLK/6of5K9Ns0c/bT9m7HMBhEZwAAACcmMlkyrOnE4CCEZwAAACc3F3REZKkPw8l6MSFNHuXAzgkghMAAICTi6jgrTa1giVJi7actHc5gEMiOAEAAMC6p9N3m4/LMAx7lwM4HIITAAAA1KNJZXm4umjfmYvafSrZ3uUADofgBAAAAAV6u+vmhqESTSKAAhGcAAAAIEnW7noLt5yQ2cJ0PeByBCcAAABIkrrUr6RAb3edScrQs3O3KjmdDXGBXAQnAAAASJI83Vw1pnt9mUzS/E0ndPsHq7ThSIK9ywIcAsEJAAAAVg+1qa7Zj7dVlQreOpaQpr4fr9X4X/cqy2yxd2mAXRGcAAAAkEermsH6eWRH3dO8iiyGNHnZAd370R86dPaivUsD7IbgBAAAgHwCvNw1oV+0PnyguQK93bX1eKLu+GC1Zq6LZZ8nOCWT4WR3flJSkgIDA5WYmKiAgAB7lwMAAODwTiWmacycrVpz4JwkqVvDUL3dp5lC/DyLdT3DMLTteKISUjJLuFKUJe3qVJSnm6tda7AlGxCcAAAAUCiLxdD0NYf17uK9yjRbFOLnoXfvbaabGoTZdJ0LqZka+90O/bj9VKnVirJh/dibFervZdcabMkGbtetKgAAAJRZLi4mPdqxltrXCdHIWVu090yyhnyxQQ+2qaaxtzeSt0fhIwer98frmTlbdCYpQ24uJjWsHCCT6bqUDwfk5lK2Vg0x4gQAAACbpGeZ9d4ve/XZ6sOSpFqVfDWpX3M1rRpYIucD1wtT9a6C4AQAAFAy/jmCNOqWevpX59pydfl7GGn3qSTrCJVy2p3/5/aGRRqhAkobwekqCE4AAAAl50Jqpv7z3Xb9tP20JOnGGkGa0DdaVSp4l8iaKKA0EZyuguAEAABQsgzD0PxNJzRu0U5dzMiWn6eb6oX5aVPsBakEuvABpYXmEAAAALhuTCaT+rSoqlY1gzVq9hZtOHpem2IvyNvdVS/d2Uj3t4qUiS4QKOMITgAAACgRkcE+mv1EW322+pC2Hk/UM7fUU61KfvYuCygRBCcAAACUGFcXkx7vVNveZQAlrmw1TwcAAAAAOyA4AQAAAEAhCE4AAAAAUAiCEwAAAAAUguAEAAAAAIUgOAEAAABAIQhOAAAAAFAIghMAAAAAFILgBAAAAACFIDgBAAAAQCEITgAAAABQCIITAAAAABSC4AQAAAAAhSA4AQAAAEAhCE4AAAAAUAiCEwAAAAAUguAEAAAAAIUgOAEAAABAIdzsXcD1ZhiGJCkpKcnepQAAAACwo9xMkJsRrsbpglNycrIkKTIy0t6lAAAAAHAAycnJCgwMvOo5JqMo8aocsVgsOnnypPz9/WUymUr1vZKSkhQZGaljx44pICCgVN8LKC7uU5QF3KdwdNyjKAu4T/MzDEPJycmKiIiQi8vVVzE53YiTi4uLqlatel3fMyAggJsTDo/7FGUB9ykcHfcoygLu07wKG2nKRXMIAAAAACgEwQkAAAAACkFwKkWenp4aN26cPD097V0KcEXcpygLuE/h6LhHURZwn14bp2sOAQAAAAC2YsQJAAAAAApBcAIAAACAQhCcAAAAAKAQBCcAAAAAKATBqRRNmTJFNWrUkJeXl1q3bq3169fbuyQ4qZiYGN14443y9/dXaGioevfurb179+Y5Jz09XcOGDVPFihXl5+enPn366MyZM3arGc7t7bfflslk0siRI63HuEfhKE6cOKEHH3xQFStWlLe3t5o2baoNGzZYnzcMQy+//LIqV64sb29vdevWTfv377drzXAeZrNZL730kmrWrClvb2/Vrl1br7/+ui7vB8c9WjwEp1Iye/ZsjR49WuPGjdOmTZsUFRWl7t27Ky4uzt6lwQmtXLlSw4YN059//qklS5YoKytLt956q1JSUqznjBo1St9//73mzJmjlStX6uTJk7rnnnvsWjec019//aWPP/5YzZo1y3OcexSO4Pz582rfvr3c3d31888/a9euXRo/fryCgoKs57z77rv64IMPNG3aNK1bt06+vr7q3r270tPT7Vo7nMM777yjjz76SB9++KF2796td955R++++64mT55sPYd7tJgMlIpWrVoZw4YNs35tNpuNiIgIIyYmxq51AYZhGHFxcYYkY+XKlYZhGMaFCxcMd3d3Y86cOdZzdu/ebUgy1q5da8dK4WySk5ONunXrGkuWLDE6d+5sjBgxwjC4R+FA/v3vfxsdOnS44vMWi8UIDw833nvvPeuxCxcuGJ6ensY333xznaqEM7vjjjuMIUOG5Dl2zz33GAMGDDAM7tFrwohTKcjMzNTGjRvVrVs36zEXFxd169ZNa9eutWttgCQlJiZKkoKDgyVJGzduVFZWVp57tkGDBqpWrRr3LK6rYcOG6Y477shzL4p7FA5k0aJFatmype677z6FhoaqefPm+vTTT63PHz58WKdPn85zrwYGBqp169bcq7gu2rVrp6VLl2rfvn2SpK1bt2r16tW67bbbJO7Ra+Jm7wLKo/j4eJnNZoWFheU5HhYWpj179titLkCSLBaLRo4cqfbt26tJkyaSpNOnT8vDw0MVKlTIc25YWJhOnz5tp0rhbGbNmqVNmzbpr7/+yvcc9ygcxaFDh/TRRx9p9OjR+s9//qO//vpLTz/9tDw8PDRo0CDr/VjQzwDcq7genn/+eSUlJalBgwZydXWV2WzWm2++qQEDBkg5/z0V92ixEJwAJzNs2DDt2LFDq1evtncpgNWxY8c0YsQILVmyRF5eXvYuB7gii8Wili1b6q233pIkNW/eXDt27NC0adM0aNAge5cH6Ntvv9WMGTM0c+ZMNW7cWFu2bNHIkSMVERHBPXqNmKpXCkJCQuTq6pqv29OZM2cUHh5ut7qA4cOH64cfftDy5ctVtWpV6/Hw8HBlZmbqwoULec7nnsX1snHjRsXFxemGG26Qm5ub3NzctHLlSn3wwQdyc3NTWFgY9ygcQuXKldWoUaM8xxo2bKjY2Fgp57+nyrk3L8e9iuvl2Wef1fPPP6/+/furadOmeuihhzRq1CjFxMRI3KPXhOBUCjw8PNSiRQstXbrUesxisWjp0qVq27atXWuDczIMQ8OHD9d3332nZcuWqWbNmnmeb9Gihdzd3fPcs3v37lVsbCz3LK6Lm2++Wdu3b9eWLVusj5YtW2rAgAHWP3OPwhG0b98+33YO+/btU/Xq1SVJNWvWVHh4eJ57NSkpSevWreNexXWRmpoqF5e8P+K7urrKYrFI3KPXhKl6pWT06NEaNGiQWrZsqVatWmnixIlKSUnR4MGD7V0anNCwYcM0c+ZMLVy4UP7+/tY5zIGBgfL29lZgYKAeeeQRjR49WsHBwQoICNBTTz2ltm3bqk2bNvYuH07A39/fuuYul6+vrypWrGg9zj0KRzBq1Ci1a9dOb731lvr27av169frk08+0SeffCJJ1v3H3njjDdWtW1c1a9bUSy+9pIiICPXu3dve5cMJ9OzZU2+++aaqVaumxo0ba/PmzZowYYKGDBkicY9eG3u39SvPJk+ebFSrVs3w8PAwWrVqZfz555/2LglOSlKBj88//9x6TlpamjF06FAjKCjI8PHxMe6++27j1KlTdq0bzu3yduQG9ygcyPfff280adLE8PT0NBo0aGB88skneZ63WCzGSy+9ZISFhRmenp7GzTffbOzdu9du9cK5JCUlGSNGjDCqVatmeHl5GbVq1TLGjh1rZGRkWM/hHi0ek3H5NsIAAAAAgHxY4wQAAAAAhSA4AQAAAEAhCE4AAAAAUAiCEwAAAAAUguAEAAAAAIUgOAEAAABAIQhOAAAAAFAIghMAAAAAFILgBACwSZcuXTRy5Mjr+p5ffPGFKlSocF3ea+/evQoPD1dycnKJvndJf98WL16s6OhoWSyWErsmAODKCE4AAFzmhRde0FNPPSV/f39JUr9+/bRv3z57l5VPjx495O7urhkzZti7FABwCgQnAAByxMbG6ocfftDDDz9sPebt7a3Q0FC71nUlDz/8sD744AN7lwEAToHgBAC4Jj/++KMCAwMLHPmwWCyqWrWqPvroozzHN2/eLBcXFx09elSSNGHCBDVt2lS+vr6KjIzU0KFDdfHixSu+58MPP6zevXvnOTZy5Eh16dIlz3vHxMSoZs2a8vb2VlRUlObOnXvVz/Ltt98qKipKVapUsR7751S9V155RdHR0frf//6nGjVqKDAwUP3797dO7ZOklJQUDRw4UH5+fqpcubLGjx+f770yMjI0ZswYValSRb6+vmrdurVWrFghSUpPT1fjxo31+OOPW88/ePCg/P39NX36dOuxnj17asOGDTp48OBVPxcA4NoRnAAAxTZz5kzdf//9mjFjhgYMGJDveRcXF91///2aOXNmnuMzZsxQ+/btVb16det5H3zwgXbu3Kkvv/xSy5Yt03PPPXdNtcXExOirr77StGnTtHPnTo0aNUoPPvigVq5cecXXrFq1Si1btiz02gcPHtSCBQv0ww8/6IcfftDKlSv19ttvW59/9tlntXLlSi1cuFC//vqrVqxYoU2bNuW5xvDhw7V27VrNmjVL27Zt03333acePXpo//798vLy0owZM/Tll19q4cKFMpvNevDBB3XLLbdoyJAh1mtUq1ZNYWFhWrVqVbG/TwCAonGzdwEAgLJpypQpGjt2rL7//nt17tz5iucNGDBA48ePV2xsrKpVqyaLxaJZs2bpxRdftJ5zedOEGjVq6I033tC//vUvTZ06tVi1ZWRk6K233tJvv/2mtm3bSpJq1aql1atX6+OPP75ivUePHi1ScLJYLPriiy+s66AeeughLV26VG+++aYuXryozz77TF9//bVuvvlmSdKXX36pqlWrWl8fGxurzz//XLGxsYqIiJAkjRkzRosXL9bnn3+ut956S9HR0XrjjTf06KOPqn///jp69Kh++OGHfLVERERYR+4AAKWH4AQAsNncuXMVFxenNWvW6MYbb7zqudHR0WrYsKFmzpyp559/XitXrlRcXJzuu+8+6zm//fabYmJitGfPHiUlJSk7O1vp6elKTU2Vj4+PzfUdOHBAqampuuWWW/Icz8zMVPPmza/4urS0NHl5eRV6/Ro1alhDkyRVrlxZcXFxUs5oVGZmplq3bm19Pjg4WPXr17d+vX37dpnNZtWrVy/PdTMyMlSxYkXr188884wWLFigDz/8UD///HOe53J5e3srNTW10JoBANeG4AQAsFnz5s21adMmTZ8+XS1btpTJZLrq+QMGDLAGp5kzZ6pHjx7WEHDkyBHdeeedevLJJ/Xmm28qODhYq1ev1iOPPKLMzMwCg5OLi4sMw8hzLCsry/rn3PVRP/74Y571SpLk6el5xTpDQkJ0/vz5Qj+/u7t7nq9NJpNNbcEvXrwoV1dXbdy4Ua6urnme8/Pzs/45Li5O+/btk6urq/bv368ePXrku1ZCQoIqVapU5PcGABQPa5wAADarXbu2li9froULF+qpp54q9PwHHnhAO3bs0MaNGzV37tw866E2btwoi8Wi8ePHq02bNqpXr55Onjx51etVqlRJp06dynNsy5Yt1j83atRInp6eio2NVZ06dfI8IiMjr3jd5s2ba9euXYV+nqupXbu23N3dtW7dOuux8+fP52lp3rx5c5nNZsXFxeWrLzw83HrekCFD1LRpU3355Zf697//rd27d+d5r/T0dB08ePCqo2gAgJLBiBMAoFjq1aun5cuXq0uXLnJzc9PEiROveG6NGjXUrl07PfLIIzKbzerVq5f1uTp16igrK0uTJ09Wz549tWbNGk2bNu2q733TTTfpvffe01dffaW2bdvq66+/1o4dO6wBwt/fX2PGjNGoUaNksVjUoUMHJSYmas2aNQoICNCgQYMKvG737t316KOPymw25xsJKio/Pz898sgjevbZZ1WxYkWFhoZq7NixcnH5+3eV9erV04ABAzRw4ECNHz9ezZs319mzZ7V06VI1a9ZMd9xxh6ZMmaK1a9dq27ZtioyM1I8//qgBAwbozz//lIeHhyTpzz//lKenp3UdFwCg9DDiBAAotvr162vZsmX65ptv9Mwzz1z13AEDBmjr1q26++675e3tbT0eFRWlCRMm6J133lGTJk00Y8YMxcTEXPVa3bt310svvaTnnntON954o5KTkzVw4MA857z++ut66aWXFBMTo4YNG6pHjx768ccfVbNmzSte97bbbpObm5t+++23In8PCvLee++pY8eO6tmzp7p166YOHTqoRYsWec75/PPPNXDgQD3zzDOqX7++evfurb/++kvVqlXTnj179Oyzz2rq1KnWEbKpU6cqPj5eL730kvUa33zzjQYMGFCsdWAAANuYjH9OEgcAwIlNmTJFixYt0i+//GLvUq4qPj5e9evX14YNG64aBgEAJYOpegAAXOaJJ57QhQsXlJycnKdznqM5cuSIpk6dSmgCgOuEEScAAAAAKARrnAAAAACgEAQnAAAAACgEwQkAAAAACkFwAgAAAIBCEJwAAAAAoBAEJwAAAAAoBMEJAAAAAApBcAIAAACAQhCcAAAAAKAQ/w9lVreHBQJq8AAAAABJRU5ErkJggg==", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "################################################\n", "# BLOCK 29: USING CROSS VALIDATION\n", "################################################\n", "\n", "#\n", "# to do this, we use \"cross validation\"\n", "# (see slide 45)\n", "#\n", "\n", "import seaborn as sns\n", "import matplotlib.pyplot as plt\n", "from sklearn.model_selection import cross_val_score\n", "\n", "#\n", "# cross-validation splits the training set into two pieces:\n", "# + model-building and model-validation. We'll use \"build\" and \"validate\"\n", "#\n", "\n", "max_k = 85\n", "all_accuracies = []\n", "best_k = 1\n", "best_accuracy = -1\n", "\n", "for k in range(1, max_k, 1):\n", " knn_cv_model = KNeighborsClassifier(n_neighbors=k)\n", " cv_scores = cross_val_score(knn_cv_model, X_train, y_train, cv=5)\n", " this_cv_accuracy = cv_scores.mean()\n", " print(f\"k: {k:2d} cv accuracy: {this_cv_accuracy:7.4f}\")\n", " all_accuracies.append(this_cv_accuracy)\n", " if this_cv_accuracy > best_accuracy:\n", " best_accuracy = this_cv_accuracy\n", " best_k = k\n", "# assign best value of k to best_k\n", "#best_k = k # *** AT THE MOMENT THIS IS INCORRECT ***\n", "# you'll need to modify the loop above to find and remember the real best_k\n", "\n", "print(f\"best_k = {best_k} yields the highest average cv accuracy.\") # print the best one\n", "\n", "plt.figure(figsize=(10, 6))\n", "sns.lineplot(x=range(1,len(all_accuracies)+1), y=all_accuracies)\n", "plt.xlabel(\"k value (index)\")\n", "plt.ylabel(\"Cross-validated accuracy\")\n", "plt.title(\"kNN Accuracy vs k Value\")\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "id": "pJW6c8XdKPSF" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Created + trained a knn classifier, now tuned with a (best) k of 28\n" ] } ], "source": [ "################################################\n", "# BLOCK 30: RE-BUILD & RE-TRAIN USING BEST k\n", "################################################\n", "\n", "#\n", "# Now, we re-create and re-run the \"Model-building and -training Cell\"\n", "#\n", "# Now, using best_k instead of the original, randomly-guessed value How does it do?!\n", "#\n", "from sklearn.neighbors import KNeighborsClassifier\n", "knn_model_tuned = KNeighborsClassifier(n_neighbors = best_k) # here, we use the best_k\n", "\n", "# we train the model (one line!)\n", "knn_model_tuned.fit(X_train, y_train) # yay! trained!\n", "print(f\"Created + trained a knn classifier, now tuned with a (best) k of {best_k}\")" ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "id": "9h5-1TukKPSF" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Predicted labels: [1. 1. 1. 1. 1. 0. 1. 2. 2. 2. 0. 0. 0. 0. 0. 1. 0. 0. 1. 0. 2. 2. 1. 2.\n", " 2. 0. 2. 1. 2.]\n", "Actual labels: [1. 1. 1. 1. 2. 0. 1. 2. 2. 2. 0. 0. 0. 0. 0. 1. 0. 0. 1. 0. 2. 2. 1. 2.\n", " 2. 0. 2. 1. 2.]\n", "\n", "row 0 : versicolor versicolor \n", "row 1 : versicolor versicolor \n", "row 2 : versicolor versicolor \n", "row 3 : versicolor versicolor \n", "row 4 : versicolor virginica incorrect\n", "row 5 : setosa setosa \n", "row 6 : versicolor versicolor \n", "row 7 : virginica virginica \n", "row 8 : virginica virginica \n", "row 9 : virginica virginica \n", "row 10 : setosa setosa \n", "row 11 : setosa setosa \n", "row 12 : setosa setosa \n", "row 13 : setosa setosa \n", "row 14 : setosa setosa \n", "row 15 : versicolor versicolor \n", "row 16 : setosa setosa \n", "row 17 : setosa setosa \n", "row 18 : versicolor versicolor \n", "row 19 : setosa setosa \n", "row 20 : virginica virginica \n", "row 21 : virginica virginica \n", "row 22 : versicolor versicolor \n", "row 23 : virginica virginica \n", "row 24 : virginica virginica \n", "row 25 : setosa setosa \n", "row 26 : virginica virginica \n", "row 27 : versicolor versicolor \n", "row 28 : virginica virginica \n", "\n", "Correct: 28 out of 29\n" ] }, { "data": { "text/plain": [ "28" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "################################################\n", "# BLOCK 31: RE-TEST THE BEST-k MODEL\n", "################################################\n", "\n", "#\n", "# Re-create and re-run the \"Model-testing Cell\" How does it do with best_k?!\n", "#\n", "predicted_labels = knn_model_tuned.predict(X_test)\n", "actual_labels = y_test\n", "\n", "# Let's print them so we can compare...\n", "print(\"Predicted labels:\", predicted_labels)\n", "print(\"Actual labels:\", actual_labels)\n", "print()\n", "# and, we'll print our nicer table...\n", "compareLabels(predicted_labels,actual_labels)" ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "id": "8dIjchI0RQ57" }, "outputs": [], "source": [ "################################################\n", "# BLOCK 32: TRYING DIFFERENT PERMUTATIONS\n", "################################################\n", "\n", "#\n", "# Before moving on, go back and choose a new permutation to give\n", "# new testing and training sets, and go back through the steps\n", "# for identifying the \"best\" k (starting at Block 22 and re-doing\n", "# up through Block 31).\n", "#\n", "# Repeat this several times.\n", "# Do you get the same value for k each time? Why or why not?\n", "#" ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "id": "h1fhLBYSKPSF" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Created + trained a 'final' knn classifier, with a (best) k of 28\n" ] } ], "source": [ "####################################################\n", "# BLOCK 33: REBUILD & TRAIN USING BEST k & ALL DATA\n", "####################################################\n", "\n", "# Ok! Now we have tuned knn to use the \"best\" value of k...\n", "#\n", "# And, we should really use ALL available data to train our final predictive model\n", "# (not split into test and train as before)\n", "#\n", "\n", "knn_model_final = KNeighborsClassifier(n_neighbors=best_k) # here, we use the best_k\n", "knn_model_final.fit(X_all, y_all) # yay! trained!\n", "print(f\"Created + trained a 'final' knn classifier, with a (best) k of {best_k}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "AsC4oa2qKPSG" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "I predict virginica (2) from features [6.7, 3.3, 5.7, 2.1]\n" ] } ], "source": [ "####################################################\n", "# BLOCK 34: TRYING FINAL MODEL \"IN THE WILD\"\n", "####################################################\n", "\n", "#\n", "# final predictive model (k-nearest-neighbor), with tuned k + ALL data incorporated\n", "#\n", "\n", "def predictiveModel( features: list[float] ) -> str:\n", " ''' input: a list of four features\n", " [ sepallen, sepalwid, petallen, petalwid ]\n", " output: the predicted species of iris, from\n", " setosa (0), versicolor (1), virginica (2)\n", " '''\n", " our_features = np.asarray([features]) # extra brackets needed\n", " predicted_species = knn_model_final.predict(our_features)\n", "\n", " predicted_species = int(round(predicted_species[0])) # unpack one element\n", " name = species_names[predicted_species]\n", " return f\"{name} ({predicted_species})\"\n", "\n", "#\n", "# Try it on several!\n", "#\n", "# features = eval(input(\"Enter new features: \"))\n", "#\n", "features = [6.7,3.3,5.7,2.1] # [5.8,2.7,4.1,1.0] [4.6,3.6,3.0,2.2] [6.7,3.3,5.7,2.1]\n", "result = predictiveModel( features )\n", "print(f\"I predict {result} from features {features}\")" ] }, { "cell_type": "code", "execution_count": 36, "metadata": { "id": "UHNILKXiRQ58" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "['setosa', 'versicolor', 'virginica']\n", "Length: 3, dtype: str\n", "======================================================================\n", " sepallen sepalwid petallen petalwid irisname\n", "1 4.9 3.0 1.4 0.2 0\n", "2 4.7 3.2 1.3 0.2 0\n", "3 4.6 3.1 1.5 0.2 0\n", "4 5.0 3.6 1.4 0.2 0\n", ".. ... ... ... ... ...\n", "144 6.7 3.3 5.7 2.5 2\n", "145 6.7 3.0 5.2 2.3 2\n", "146 6.3 2.5 5.0 1.9 2\n", "147 6.5 3.0 5.2 2.0 2\n", "\n", "[145 rows x 5 columns]\n" ] } ], "source": [ "################################################################\n", "# BLOCK 35a: HOW DOES THE MODEL PERFORM ON MEAN OF EACH FLOWER?\n", "################################################################\n", "\n", "# let's recall the species names and their order in the list\n", "print(species_names)\n", "print('=' * 70)\n", "\n", "# and let's recall what the final DataFrame looks like, noting that\n", "# the irisname was converted to the corresponding index in species_names\n", "print(df_final)" ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "id": "MlnIrLPWRQ58" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " sepallen sepalwid petallen petalwid irisname\n", "50 7.0 3.2 4.7 1.4 1\n", "51 6.4 3.2 4.5 1.5 1\n", "52 6.9 3.1 4.9 1.5 1\n", "53 5.5 2.3 4.0 1.3 1\n", ".. ... ... ... ... ...\n", "96 5.7 2.9 4.2 1.3 1\n", "97 6.2 2.9 4.3 1.3 1\n", "98 5.1 2.5 3.0 1.1 1\n", "99 5.7 2.8 4.1 1.3 1\n", "\n", "[50 rows x 5 columns]\n", "======================================================================\n", "sepallen 5.936\n", "sepalwid 2.770\n", "petallen 4.260\n", "petalwid 1.326\n", "irisname 1.000\n", "dtype: float64\n", "======================================================================\n", "[5.936 2.77 4.26 1.326]\n", "Mean versicolor features [5.936 2.77 4.26 1.326] -> versicolor (1)\n" ] } ], "source": [ "################################################################\n", "# BLOCK 35b: HOW DOES THE MODEL PERFORM ON MEAN OF EACH FLOWER?\n", "################################################################\n", "\n", "# let's grab the data from df_final corresponding to versicolor\n", "versicolor_data = df_final[ df_final['irisname'] == convertSpecies('versicolor') ]\n", "print(versicolor_data)\n", "print('=' * 70)\n", "\n", "# let's print the mean of each column\n", "print(versicolor_data.mean())\n", "print('=' * 70)\n", "\n", "# and let's grab the features means only (don't need the label) for versicolor\n", "versicolor_features_means = np.asarray(versicolor_data.mean()[:-1])\n", "print(versicolor_features_means)\n", "\n", "# now run the predictive model (see Block 34 above) and print a corresponding\n", "# message\n", "\n", "# >>> YOU ADD CODE HERE\n", "result = predictiveModel(versicolor_features_means)\n", "print(f\"Mean versicolor features {versicolor_features_means} -> {result}\")" ] }, { "cell_type": "code", "execution_count": 38, "metadata": { "id": "twR2L9IRRQ58" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Mean setosa features [5.00425532 3.43617021 1.45957447 0.24893617] -> setosa (0)\n", "Mean versicolor features [5.936 2.77 4.26 1.326] -> versicolor (1)\n", "Mean virginica features [6.61041667 2.96458333 5.56458333 2.025 ] -> virginica (2)\n" ] } ], "source": [ "################################################################\n", "# BLOCK 35c: HOW DOES THE MODEL PERFORM ON MEAN OF EACH FLOWER?\n", "################################################################\n", "\n", "# Automate the process used in Block 35b, across all species:\n", "#\n", "# Loop across species names, and for each species,\n", "# (a) pull that corresponding data from df_final\n", "# (b) compute the means of the features, storing as a numpy array\n", "# (c) call the predictive model using those features\n", "# (d) print a corresponding message\n", "\n", "# >> YOU ADD CODE HERE -- YOU SHOULD NEED NO MORE THAN 5 LINES OF CODE\n", "for name in species_names:\n", " species_data = df_final[df_final['irisname'] == convertSpecies(name)]\n", " species_features_means = np.asarray(species_data.mean()[:-1])\n", " result = predictiveModel(species_features_means)\n", " print(f\"Mean {name} features {species_features_means} -> {result}\")" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.14.3" } }, "nbformat": 4, "nbformat_minor": 0 }