From a59c966b494702067bff067ebff17961182ac2f0 Mon Sep 17 00:00:00 2001 From: Benjamin Adovasio Date: Sun, 29 Mar 2026 17:35:09 -0400 Subject: [PATCH] initial commit for HW 5 --- HW 5/Iris_knn_W2025_template.ipynb | 1771 +++++++++++++++++ ...lass work – ML Workflow (Iris Dataset).pdf | Bin 0 -> 52759 bytes HW 5/iris.csv | 151 ++ 3 files changed, 1922 insertions(+) create mode 100644 HW 5/Iris_knn_W2025_template.ipynb create mode 100644 HW 5/W2025 DCS 211_ In-class work – ML Workflow (Iris Dataset).pdf create mode 100644 HW 5/iris.csv diff --git a/HW 5/Iris_knn_W2025_template.ipynb b/HW 5/Iris_knn_W2025_template.ipynb new file mode 100644 index 0000000..929efcb --- /dev/null +++ b/HW 5/Iris_knn_W2025_template.ipynb @@ -0,0 +1,1771 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "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": { + "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", + "source": [ + "####################################\n", + "from sklearn.datasets import load_iris\n", + "data = load_iris()\n", + "df = pd.DataFrame(data.data, columns=data.feature_names)\n", + "df" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 424 + }, + "id": "DjCaxH3BKJ_O", + "outputId": "ab33e45d-d332-412e-db2a-88cc4c9528f4" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)\n", + "0 5.1 3.5 1.4 0.2\n", + "1 4.9 3.0 1.4 0.2\n", + "2 4.7 3.2 1.3 0.2\n", + "3 4.6 3.1 1.5 0.2\n", + "4 5.0 3.6 1.4 0.2\n", + ".. ... ... ... ...\n", + "145 6.7 3.0 5.2 2.3\n", + "146 6.3 2.5 5.0 1.9\n", + "147 6.5 3.0 5.2 2.0\n", + "148 6.2 3.4 5.4 2.3\n", + "149 5.9 3.0 5.1 1.8\n", + "\n", + "[150 rows x 4 columns]" + ], + "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", + "
sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)
05.13.51.40.2
14.93.01.40.2
24.73.21.30.2
34.63.11.50.2
45.03.61.40.2
...............
1456.73.05.22.3
1466.32.55.01.9
1476.53.05.22.0
1486.23.45.42.3
1495.93.05.11.8
\n", + "

150 rows × 4 columns

\n", + "
\n", + "
\n", + "\n", + "
\n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
\n", + "\n", + "\n", + "
\n", + " \n", + " \n", + " \n", + "
\n", + "\n", + "
\n", + "
\n" + ], + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "dataframe", + "variable_name": "df", + "summary": "{\n \"name\": \"df\",\n \"rows\": 150,\n \"fields\": [\n {\n \"column\": \"sepal length (cm)\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.8280661279778629,\n \"min\": 4.3,\n \"max\": 7.9,\n \"num_unique_values\": 35,\n \"samples\": [\n 6.2,\n 4.5,\n 5.6\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"sepal width (cm)\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.435866284936698,\n \"min\": 2.0,\n \"max\": 4.4,\n \"num_unique_values\": 23,\n \"samples\": [\n 2.3,\n 4.0,\n 3.5\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"petal length (cm)\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.7652982332594667,\n \"min\": 1.0,\n \"max\": 6.9,\n \"num_unique_values\": 43,\n \"samples\": [\n 6.7,\n 3.8,\n 3.7\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"petal width (cm)\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.7622376689603465,\n \"min\": 0.1,\n \"max\": 2.5,\n \"num_unique_values\": 22,\n \"samples\": [\n 0.2,\n 1.2,\n 1.3\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}" + } + }, + "metadata": {}, + "execution_count": 9 + } + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ANJhDRNPKPR5" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "8v3oVZYTKPR6", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 361 + }, + "outputId": "4110ac58-5c52-4cbf-a83f-16d9a7189ccb" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + " sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)\n", + "0 5.1 3.5 1.4 0.2\n", + "1 4.9 3.0 1.4 0.2\n", + "2 4.7 3.2 1.3 0.2\n", + "3 4.6 3.1 1.5 0.2\n", + ".. ... ... ... ...\n", + "146 6.3 2.5 5.0 1.9\n", + "147 6.5 3.0 5.2 2.0\n", + "148 6.2 3.4 5.4 2.3\n", + "149 5.9 3.0 5.1 1.8\n", + "\n", + "[150 rows x 4 columns]" + ], + "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", + "
sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)
05.13.51.40.2
14.93.01.40.2
24.73.21.30.2
34.63.11.50.2
...............
1466.32.55.01.9
1476.53.05.22.0
1486.23.45.42.3
1495.93.05.11.8
\n", + "

150 rows × 4 columns

\n", + "
\n", + "
\n", + "\n", + "
\n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
\n", + "\n", + "\n", + "
\n", + " \n", + " \n", + " \n", + "
\n", + "\n", + "
\n", + "
\n" + ], + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "dataframe", + "variable_name": "df", + "summary": "{\n \"name\": \"df\",\n \"rows\": 150,\n \"fields\": [\n {\n \"column\": \"sepal length (cm)\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.8280661279778629,\n \"min\": 4.3,\n \"max\": 7.9,\n \"num_unique_values\": 35,\n \"samples\": [\n 6.2,\n 4.5,\n 5.6\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"sepal width (cm)\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.435866284936698,\n \"min\": 2.0,\n \"max\": 4.4,\n \"num_unique_values\": 23,\n \"samples\": [\n 2.3,\n 4.0,\n 3.5\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"petal length (cm)\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.7652982332594667,\n \"min\": 1.0,\n \"max\": 6.9,\n \"num_unique_values\": 43,\n \"samples\": [\n 6.7,\n 3.8,\n 3.7\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"petal width (cm)\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.7622376689603465,\n \"min\": 0.1,\n \"max\": 2.5,\n \"num_unique_values\": 22,\n \"samples\": [\n 0.2,\n 1.2,\n 1.3\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}" + } + }, + "metadata": {}, + "execution_count": 10 + } + ], + "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": null, + "metadata": { + "id": "aksGQcJ8KPR6", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "4cbdedf5-cabe-4263-c06c-26bbb8f509b1" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "RangeIndex: 150 entries, 0 to 149\n", + "Data columns (total 4 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 sepal length (cm) 150 non-null float64\n", + " 1 sepal width (cm) 150 non-null float64\n", + " 2 petal length (cm) 150 non-null float64\n", + " 3 petal width (cm) 150 non-null float64\n", + "dtypes: float64(4)\n", + "memory usage: 4.8 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": null, + "metadata": { + "id": "ADWpUqeKKPR7", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "76ac8d2f-0d98-4721-e0f2-b79b48736b00" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "RangeIndex: 150 entries, 0 to 149\n", + "Data columns (total 4 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 sepal length (cm) 150 non-null float64\n", + " 1 sepal width (cm) 150 non-null float64\n", + " 2 petal length (cm) 150 non-null float64\n", + " 3 petal width (cm) 150 non-null float64\n", + "dtypes: float64(4)\n", + "memory usage: 4.8 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 by name is typical, but what else is possible?\n", + "df_clean.info() # Is the bad last column gone?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "9RAydRsiKPR7", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "18ec37ec-3a95-4ef9-c111-847ac8f6ecec" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "FEATURES: Index(['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)',\n", + " 'petal width (cm)'],\n", + " dtype='object')\n", + "\n", + "First feature: sepal length (cm)\n", + "\n", + "feature_name_to_index: {'sepal length (cm)': 0, 'sepal width (cm)': 1, 'petal length (cm)': 2, 'petal width (cm)': 3}\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": null, + "metadata": { + "id": "PWkPTOGnKPR8", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "outputId": "f6a46e8f-0169-4646-864f-1b3ed749f238" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "\n", + "RangeIndex: 150 entries, 0 to 149\n", + "Data columns (total 4 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 sepal length (cm) 150 non-null float64\n", + " 1 sepal width (cm) 150 non-null float64\n", + " 2 petal length (cm) 150 non-null float64\n", + " 3 petal width (cm) 150 non-null float64\n", + "dtypes: float64(4)\n", + "memory usage: 4.8 KB\n", + "======================================================================\n", + " sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)\n", + "0 5.1 3.5 1.4 0.2\n", + "1 4.9 3.0 1.4 0.2\n", + "2 4.7 3.2 1.3 0.2\n", + "3 4.6 3.1 1.5 0.2\n", + ".. ... ... ... ...\n", + "146 6.3 2.5 5.0 1.9\n", + "147 6.5 3.0 5.2 2.0\n", + "148 6.2 3.4 5.4 2.3\n", + "149 5.9 3.0 5.1 1.8\n", + "\n", + "[150 rows x 4 columns]\n", + "======================================================================\n" + ] + }, + { + "output_type": "error", + "ename": "KeyError", + "evalue": "'irisname'", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/pandas/core/indexes/base.py\u001b[0m in \u001b[0;36mget_loc\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 3804\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 3805\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_engine\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_loc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcasted_key\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3806\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mKeyError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0merr\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32mindex.pyx\u001b[0m in \u001b[0;36mpandas._libs.index.IndexEngine.get_loc\u001b[0;34m()\u001b[0m\n", + "\u001b[0;32mindex.pyx\u001b[0m in \u001b[0;36mpandas._libs.index.IndexEngine.get_loc\u001b[0;34m()\u001b[0m\n", + "\u001b[0;32mpandas/_libs/hashtable_class_helper.pxi\u001b[0m in \u001b[0;36mpandas._libs.hashtable.PyObjectHashTable.get_item\u001b[0;34m()\u001b[0m\n", + "\u001b[0;32mpandas/_libs/hashtable_class_helper.pxi\u001b[0m in \u001b[0;36mpandas._libs.hashtable.PyObjectHashTable.get_item\u001b[0;34m()\u001b[0m\n", + "\u001b[0;31mKeyError\u001b[0m: 'irisname'", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/tmp/ipykernel_6512/325098609.py\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0;31m# or more to the point, grab that column and inspect:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 18\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf\"Irisname entries: {df_clean['irisname'].unique()}\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 19\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[0;31m# also note how the last two rows in the df have problems, among others...\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/pandas/core/frame.py\u001b[0m in \u001b[0;36m__getitem__\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 4100\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnlevels\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4101\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_getitem_multilevel\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mkey\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 4102\u001b[0;31m \u001b[0mindexer\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_loc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mkey\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4103\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mis_integer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mindexer\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4104\u001b[0m \u001b[0mindexer\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mindexer\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/pandas/core/indexes/base.py\u001b[0m in \u001b[0;36mget_loc\u001b[0;34m(self, key)\u001b[0m\n\u001b[1;32m 3810\u001b[0m ):\n\u001b[1;32m 3811\u001b[0m \u001b[0;32mraise\u001b[0m \u001b[0mInvalidIndexError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mkey\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 3812\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mKeyError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mkey\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0merr\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3813\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mTypeError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3814\u001b[0m \u001b[0;31m# If we have a listlike key, _check_indexing_error will raise\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mKeyError\u001b[0m: 'irisname'" + ] + } + ], + "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", + "source": [ + "df.columns" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "4B2An3iML1ay", + "outputId": "5492db65-acd8-4c11-f669-742a79b6cb62" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "Index(['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)',\n", + " 'petal width (cm)'],\n", + " dtype='object')" + ] + }, + "metadata": {}, + "execution_count": 19 + } + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "xM22NWdqKPR-", + "scrolled": false + }, + "outputs": [], + "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": null, + "metadata": { + "id": "h0spkxGzKPR-" + }, + "outputs": [], + "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 = pass\n", + "# ^^^^ YOU NEED TO WRITE CODE HERE...\n", + "\n", + "print(df_final.shape)\n", + "df_final" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EywtvRyMKPR-" + }, + "outputs": [], + "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", + "df_final['irisname'] = df_final['irisname'].apply(convertSpecies)\n", + "\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": null, + "metadata": { + "id": "tWoVKJBGKPR_" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "qxpBs7NnKPR_" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "bblbBuIiKPSA" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "wbns1qpJKPSA" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "ixiZhsuqKPSA", + "scrolled": true + }, + "outputs": [], + "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": null, + "metadata": { + "id": "izGld2tSKPSA" + }, + "outputs": [], + "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": null, + "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": null, + "metadata": { + "id": "64kSAiCvKPSB" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "2ChEk2CDKPSC" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "sTDi2mFnKPSC" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "_pD7IZR1KPSC" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "jvJMYC3eKPSD" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "d3cB0xryKPSD" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "liXBT5-IKPSD" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "h_ZigXq2KPSE" + }, + "outputs": [], + "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": null, + "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": null, + "metadata": { + "id": "ZUmotr7hKPSE" + }, + "outputs": [], + "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", + "\n", + "for k in range(1, max_k, 1):\n", + " knn_cv_model = KNeighborsClassifier(n_neighbors = k) # build knn_model for every k!\n", + " cv_scores = cross_val_score( knn_cv_model, X_train, y_train, cv = 5 ) # 5 means 80/20 split\n", + " this_cv_accuracy = cv_scores.mean() # mean() is numpy's built-in average function\n", + " print(f\"k: {k:2d} cv accuracy: {this_cv_accuracy:7.4f}\")\n", + " all_accuracies.append(this_cv_accuracy)\n", + "\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": null, + "metadata": { + "id": "pJW6c8XdKPSF" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "9h5-1TukKPSF" + }, + "outputs": [], + "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": null, + "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": null, + "metadata": { + "id": "h1fhLBYSKPSF" + }, + "outputs": [], + "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": [], + "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] ) -> None:\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": null, + "metadata": { + "id": "UHNILKXiRQ58" + }, + "outputs": [], + "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": null, + "metadata": { + "id": "MlnIrLPWRQ58" + }, + "outputs": [], + "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" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "twR2L9IRRQ58" + }, + "outputs": [], + "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" + ] + } + ], + "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.12.2" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/HW 5/W2025 DCS 211_ In-class work – ML Workflow (Iris Dataset).pdf b/HW 5/W2025 DCS 211_ In-class work – ML Workflow (Iris Dataset).pdf new file mode 100644 index 0000000000000000000000000000000000000000..b910bca6e2aef4f1ecf610f3dcd59d073f590cd2 GIT binary patch literal 52759 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