{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Which Penguin Is This?\n",
    "\n",
    "penguin_species.py\n",
    "\n",
    "AI project on Statistical Data: predicting a penguin's species from four\n",
    "measurements of its body.\n",
    "\n",
    "CBSE Class X, Artificial Intelligence (417), Part C project work.\n",
    "SDG 14 (Life below Water) and SDG 15 (Life on Land).\n",
    "\n",
    "The program follows the six stages of the AI Project Cycle, in order:\n",
    "  1. Problem scoping      2. Data acquisition     3. Data exploration\n",
    "  4. Modelling            5. Evaluation           6. Deployment\n",
    "\n",
    "It builds two models and compares them:\n",
    "  - a RULE BASED model, whose rules we write ourselves after looking at the\n",
    "    charts, and\n",
    "  - a LEARNING BASED model, which works out the \"typical\" penguin of each\n",
    "    species from the training data on its own.\n",
    "\n",
    "Libraries: pandas (the data), numpy (the arithmetic), matplotlib (charts)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 1 - Problem scoping"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"=\" * 60)\n",
    "print(\"PENGUIN SPECIES PREDICTION\")\n",
    "print(\"=\" * 60)\n",
    "print(\"Who   : field scientists counting penguins in Antarctica\")\n",
    "print(\"What  : telling three similar species apart takes an expert\")\n",
    "print(\"Where : on the islands, during the breeding season\")\n",
    "print(\"Why   : species counts show how each one is coping with change\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 2 - Data acquisition"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# Show every column when a table is printed, instead of \"...\" in the middle.\n",
    "pd.set_option(\"display.width\", 120)\n",
    "pd.set_option(\"display.max_columns\", 20)\n",
    "\n",
    "data = pd.read_csv(\"penguins.csv\")\n",
    "\n",
    "print(\"--- Stage 2: Data acquisition ---\")\n",
    "print(\"Rows read    :\", len(data))\n",
    "print(\"Columns read :\", len(data.columns))\n",
    "print()\n",
    "print(\"The first 10 rows:\")\n",
    "print(data.head(10))\n",
    "print()\n",
    "data.info()\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 3 - Data exploration"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Stage 3: Data exploration ---\")\n",
    "\n",
    "# The four measurements the model will use. These are the FEATURES; the\n",
    "# species is the LABEL - the answer the model has to predict.\n",
    "features = [\"bill_length_mm\", \"bill_depth_mm\", \"flipper_length_mm\", \"body_mass_g\"]\n",
    "label = \"species\"\n",
    "\n",
    "# A penguin with a measurement missing cannot be compared with anything, so\n",
    "# that row is removed. The count is printed so nothing disappears silently.\n",
    "missing = data[features].isnull().any(axis=1).sum()\n",
    "data = data.dropna(subset=features)\n",
    "print(\"Rows with a measurement missing :\", missing, \"(removed)\")\n",
    "print(\"Rows left                       :\", len(data))\n",
    "print()\n",
    "\n",
    "print(\"Penguins of each species:\")\n",
    "counts = data[label].value_counts()\n",
    "print(counts)\n",
    "print()\n",
    "\n",
    "# Mean, median and mode of one feature, worked out with numpy and pandas.\n",
    "flipper = data[\"flipper_length_mm\"]\n",
    "print(\"Flipper length - mean   :\", round(np.mean(flipper), 1), \"mm\")\n",
    "print(\"Flipper length - median :\", np.median(flipper), \"mm\")\n",
    "print(\"Flipper length - mode   :\", flipper.mode()[0], \"mm\")\n",
    "print()\n",
    "\n",
    "print(\"Average measurements of each species:\")\n",
    "print(data.groupby(label)[features].mean().round(1))\n",
    "print()\n",
    "\n",
    "colours = {\"Adelie\": \"#f28e2b\", \"Chinstrap\": \"#9467bd\", \"Gentoo\": \"#17a2b8\"}\n",
    "species_list = [\"Adelie\", \"Chinstrap\", \"Gentoo\"]\n",
    "\n",
    "# Chart 1: how many of each species - is the data balanced?\n",
    "plt.figure(figsize=(6, 4))\n",
    "plt.bar(species_list, counts.reindex(species_list, fill_value=0),\n",
    "        color=[colours[\"Adelie\"], colours[\"Chinstrap\"], colours[\"Gentoo\"]])\n",
    "plt.title(\"Penguins of each species in the dataset\")\n",
    "plt.xlabel(\"Species\")\n",
    "plt.ylabel(\"Number of penguins\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart1_species_count.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 2: flipper length against body mass, one colour per species.\n",
    "plt.figure(figsize=(7, 5))\n",
    "for s in species_list:\n",
    "    rows = data[data[label] == s]\n",
    "    plt.scatter(rows[\"flipper_length_mm\"], rows[\"body_mass_g\"],\n",
    "                color=colours[s], label=s, alpha=0.75)\n",
    "plt.title(\"Flipper length and body mass\")\n",
    "plt.xlabel(\"Flipper length (mm)\")\n",
    "plt.ylabel(\"Body mass (g)\")\n",
    "plt.legend()\n",
    "plt.grid(True, linestyle=\"--\", alpha=0.4)\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart2_flipper_mass.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 3: bill length against bill depth.\n",
    "plt.figure(figsize=(7, 5))\n",
    "for s in species_list:\n",
    "    rows = data[data[label] == s]\n",
    "    plt.scatter(rows[\"bill_length_mm\"], rows[\"bill_depth_mm\"],\n",
    "                color=colours[s], label=s, alpha=0.75)\n",
    "plt.title(\"Bill length and bill depth\")\n",
    "plt.xlabel(\"Bill length (mm)\")\n",
    "plt.ylabel(\"Bill depth (mm)\")\n",
    "plt.legend()\n",
    "plt.grid(True, linestyle=\"--\", alpha=0.4)\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart3_bill.png\")\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 4 - Modelling"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Stage 4: Modelling ---\")\n",
    "\n",
    "# Train-test split. The rows are shuffled (random_state fixes the shuffle so\n",
    "# every run is the same), then 80% are used to TRAIN and 20% are kept aside\n",
    "# to TEST. The model never sees the testing rows while it is being built.\n",
    "data = data.sample(frac=1, random_state=7)\n",
    "cut = int(len(data) * 0.8)\n",
    "train = data[:cut]\n",
    "test = data[cut:]\n",
    "print(\"Training rows :\", len(train))\n",
    "print(\"Testing rows  :\", len(test))\n",
    "print()\n",
    "\n",
    "# Model A - RULE BASED. These two rules were written by hand after looking\n",
    "# at charts 2 and 3: Gentoo have the longest flippers, and of the other two,\n",
    "# Chinstrap have the longer bill.\n",
    "rule_predictions = []\n",
    "for i in range(len(test)):\n",
    "    row = test.iloc[i]\n",
    "    if row[\"flipper_length_mm\"] >= 206:\n",
    "        rule_predictions.append(\"Gentoo\")\n",
    "    elif row[\"bill_length_mm\"] >= 44:\n",
    "        rule_predictions.append(\"Chinstrap\")\n",
    "    else:\n",
    "        rule_predictions.append(\"Adelie\")\n",
    "\n",
    "# Model B - LEARNING BASED. The model learns the average penguin of each\n",
    "# species from the training rows, then labels a new penguin with whichever\n",
    "# average it is closest to.\n",
    "#\n",
    "# Body mass is in thousands and bill depth is under 25, so measured as they\n",
    "# are, mass would drown out everything else. Each feature is first rescaled\n",
    "# to run from 0 to 1, using the smallest and largest value in TRAINING data.\n",
    "low = train[features].min()\n",
    "high = train[features].max()\n",
    "train_scaled = (train[features] - low) / (high - low)\n",
    "test_scaled = (test[features] - low) / (high - low)\n",
    "\n",
    "averages = train_scaled.groupby(train[label]).mean()\n",
    "print(\"What the model learned - the average penguin of each species\")\n",
    "print(\"(every feature rescaled to run from 0 to 1):\")\n",
    "print(averages.round(2))\n",
    "print()\n",
    "\n",
    "learned_predictions = []\n",
    "for i in range(len(test)):\n",
    "    row = test_scaled.iloc[i]\n",
    "    # Distance from this penguin to each species' average: the gaps are\n",
    "    # squared and added up. The smallest total is the closest species.\n",
    "    distance = ((averages - row) ** 2).sum(axis=1)\n",
    "    learned_predictions.append(distance.idxmin())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 5 - Evaluation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Stage 5: Evaluation ---\")\n",
    "actual = list(test[label])\n",
    "\n",
    "rule_correct = 0\n",
    "learned_correct = 0\n",
    "for i in range(len(actual)):\n",
    "    if rule_predictions[i] == actual[i]:\n",
    "        rule_correct = rule_correct + 1\n",
    "    if learned_predictions[i] == actual[i]:\n",
    "        learned_correct = learned_correct + 1\n",
    "\n",
    "rule_accuracy = rule_correct / len(actual) * 100\n",
    "learned_accuracy = learned_correct / len(actual) * 100\n",
    "print(\"Rule based model     :\", rule_correct, \"of\", len(actual), \"correct\")\n",
    "print(\"Learning based model :\", learned_correct, \"of\", len(actual), \"correct\")\n",
    "print(\"Accuracy of the rule based model     :\", round(rule_accuracy, 1), \"%\")\n",
    "print(\"Accuracy of the learning based model :\", round(learned_accuracy, 1), \"%\")\n",
    "print()\n",
    "\n",
    "# The confusion matrix: rows are what the penguin really was, columns are\n",
    "# what the model said. Everything on the diagonal is a correct prediction.\n",
    "matrix = pd.crosstab(pd.Series(actual, name=\"Actual\"),\n",
    "                     pd.Series(learned_predictions, name=\"Predicted\"))\n",
    "matrix = matrix.reindex(index=species_list, columns=species_list, fill_value=0)\n",
    "print(\"Confusion matrix of the learning based model:\")\n",
    "print(matrix)\n",
    "print()\n",
    "\n",
    "# Precision and recall for each species, read straight off the matrix.\n",
    "print(\"Species      Precision   Recall\")\n",
    "for s in species_list:\n",
    "    tp = matrix.loc[s, s]\n",
    "    predicted_as_s = matrix[s].sum()\n",
    "    really_s = matrix.loc[s].sum()\n",
    "    if predicted_as_s > 0:\n",
    "        precision = tp / predicted_as_s * 100\n",
    "    else:\n",
    "        precision = 0\n",
    "    if really_s > 0:\n",
    "        recall = tp / really_s * 100\n",
    "    else:\n",
    "        recall = 0\n",
    "    print(s.ljust(12), str(round(precision, 1)).rjust(8), \"%\", str(round(recall, 1)).rjust(7), \"%\")\n",
    "print()\n",
    "\n",
    "# Chart 4: the confusion matrix, drawn as a grid of coloured boxes.\n",
    "plt.figure(figsize=(5.5, 4.5))\n",
    "plt.imshow(matrix.values, cmap=\"Blues\")\n",
    "for r in range(len(species_list)):\n",
    "    for c in range(len(species_list)):\n",
    "        value = matrix.values[r][c]\n",
    "        if value > matrix.values.max() / 2:\n",
    "            ink = \"white\"\n",
    "        else:\n",
    "            ink = \"black\"\n",
    "        plt.text(c, r, value, ha=\"center\", va=\"center\", fontsize=14, color=ink)\n",
    "plt.xticks(range(3), species_list)\n",
    "plt.yticks(range(3), species_list)\n",
    "plt.xlabel(\"Predicted species\")\n",
    "plt.ylabel(\"Actual species\")\n",
    "plt.title(\"Confusion matrix (testing data)\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart4_confusion_matrix.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 5: the two models side by side.\n",
    "plt.figure(figsize=(6, 4))\n",
    "plt.bar([\"Rule based\", \"Learning based\"], [rule_accuracy, learned_accuracy],\n",
    "        color=[\"#999999\", \"#17a2b8\"])\n",
    "plt.ylim(0, 100)\n",
    "plt.title(\"Accuracy on the same testing data\")\n",
    "plt.ylabel(\"Accuracy (%)\")\n",
    "plt.text(0, rule_accuracy + 2, str(round(rule_accuracy, 1)) + \"%\", ha=\"center\")\n",
    "plt.text(1, learned_accuracy + 2, str(round(learned_accuracy, 1)) + \"%\", ha=\"center\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart5_models_compared.png\")\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 6 - Deployment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Stage 6: Deployment ---\")\n",
    "print(\"Three penguins measured on a new visit:\")\n",
    "\n",
    "new_penguins = pd.DataFrame({\n",
    "    \"bill_length_mm\":    [39.0, 50.5, 47.0],\n",
    "    \"bill_depth_mm\":     [18.5, 19.0, 14.8],\n",
    "    \"flipper_length_mm\": [188, 198, 215],\n",
    "    \"body_mass_g\":       [3600, 3800, 5200],\n",
    "})\n",
    "new_scaled = (new_penguins - low) / (high - low)\n",
    "\n",
    "for i in range(len(new_penguins)):\n",
    "    distance = ((averages - new_scaled.iloc[i]) ** 2).sum(axis=1)\n",
    "    m = new_penguins.iloc[i]\n",
    "    print(\"Bill\", m[\"bill_length_mm\"], \"x\", m[\"bill_depth_mm\"], \"mm, flipper\",\n",
    "          int(m[\"flipper_length_mm\"]), \"mm,\", int(m[\"body_mass_g\"]), \"g  ->\",\n",
    "          distance.idxmin())"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
