{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Will Tomorrow's Air Be Poor?\n",
    "\n",
    "air_quality_alert.py\n",
    "\n",
    "AI project on Statistical Data: warning a day ahead when the air is going\n",
    "to be \"Poor\" or worse, from today's readings at a city monitoring station.\n",
    "\n",
    "CBSE Class X, Artificial Intelligence (417), Part C project work.\n",
    "SDG 11 (Sustainable Cities) and SDG 3 (Good Health and Well-being).\n",
    "\n",
    "The model is a PERCEPTRON - the single artificial neuron of Unit 2. It\n",
    "multiplies each input by a weight, adds a bias, and says \"Poor tomorrow\"\n",
    "if the total is above 0. In Unit 2 the weights were given. Here the\n",
    "perceptron finds them itself: every time it gets a training day wrong, it\n",
    "nudges its weights a little towards the right answer, and it goes through\n",
    "the training days again and again.\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(\"WILL TOMORROW'S AIR BE POOR?\")\n",
    "print(\"=\" * 60)\n",
    "print(\"Who   : schools, parents, and people with asthma\")\n",
    "print(\"What  : bad-air days are announced on the day, too late to plan\")\n",
    "print(\"Where : a city with one air-quality monitoring station\")\n",
    "print(\"Why   : a day's warning lets people protect themselves\")\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",
    "pd.set_option(\"display.width\", 120)\n",
    "pd.set_option(\"display.max_columns\", 20)\n",
    "\n",
    "data = pd.read_csv(\"air_quality.csv\", parse_dates=[\"date\"])\n",
    "\n",
    "print(\"--- Stage 2: Data acquisition ---\")\n",
    "print(\"Rows read    :\", len(data))\n",
    "print(\"First day    :\", data[\"date\"].min().date())\n",
    "print(\"Last day     :\", data[\"date\"].max().date())\n",
    "print()\n",
    "print(\"The first 10 rows:\")\n",
    "print(data.head(10))\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",
    "# PM2.5 above 90 is \"Poor\" or worse on India's air quality scale.\n",
    "# The label is about TOMORROW, so each day is given the next day's PM2.5.\n",
    "# This must be done BEFORE any rows are removed, or a gap in the data would\n",
    "# pair a day with the wrong \"tomorrow\".\n",
    "data[\"pm25_tomorrow\"] = data[\"pm25\"].shift(-1)\n",
    "data[\"poor_tomorrow\"] = np.where(data[\"pm25_tomorrow\"] > 90, 1, 0)\n",
    "\n",
    "# A day is unusable if today's or tomorrow's reading is missing (the\n",
    "# sensor was offline). The last day has no tomorrow at all.\n",
    "unusable = data[\"pm25\"].isnull() | data[\"pm25_tomorrow\"].isnull()\n",
    "print(\"Days with a missing reading today or tomorrow :\", unusable.sum(), \"(removed)\")\n",
    "data = data[~unusable]\n",
    "print(\"Days left                                     :\", len(data))\n",
    "print()\n",
    "\n",
    "print(\"PM2.5 - mean   :\", round(np.mean(data[\"pm25\"]), 1))\n",
    "print(\"PM2.5 - median :\", np.median(data[\"pm25\"]))\n",
    "print(\"Days followed by a Poor day :\", data[\"poor_tomorrow\"].sum(), \"of\", len(data))\n",
    "print()\n",
    "\n",
    "monthly = data.groupby(data[\"date\"].dt.month)[\"pm25\"].mean().round(1)\n",
    "print(\"Average PM2.5 in each month (both years together):\")\n",
    "print(monthly.to_string())\n",
    "print()\n",
    "\n",
    "# Chart 1: PM2.5 every day for two years.\n",
    "plt.figure(figsize=(9, 4))\n",
    "plt.plot(data[\"date\"], data[\"pm25\"], color=\"#8c6d31\", linewidth=0.9)\n",
    "plt.axhline(90, color=\"#e15759\", linestyle=\"--\", label=\"90: the start of Poor\")\n",
    "plt.title(\"PM2.5 every day for two years\")\n",
    "plt.xlabel(\"Date\")\n",
    "plt.ylabel(\"PM2.5 (micrograms per cubic metre)\")\n",
    "plt.legend()\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart1_pm25_daily.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 2: today's PM2.5 and tomorrow's wind forecast.\n",
    "poor = data[data[\"poor_tomorrow\"] == 1]\n",
    "fine = data[data[\"poor_tomorrow\"] == 0]\n",
    "plt.figure(figsize=(7, 5))\n",
    "plt.scatter(fine[\"pm25\"], fine[\"wind_forecast_kmph\"], color=\"#59a14f\", alpha=0.5, s=14, label=\"Tomorrow not Poor\")\n",
    "plt.scatter(poor[\"pm25\"], poor[\"wind_forecast_kmph\"], color=\"#e15759\", alpha=0.6, s=14, label=\"Tomorrow Poor\")\n",
    "plt.title(\"Today's PM2.5, tomorrow's wind forecast, and what tomorrow brought\")\n",
    "plt.xlabel(\"PM2.5 today\")\n",
    "plt.ylabel(\"Wind forecast for tomorrow (km/h)\")\n",
    "plt.legend()\n",
    "plt.grid(True, linestyle=\"--\", alpha=0.4)\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart2_today_tomorrow.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",
    "# Today's PM2.5, humidity and temperature, plus the weather office's\n",
    "# FORECAST of tomorrow's wind - real air-quality forecasts use weather\n",
    "# forecasts too, because wind is what clears the air.\n",
    "features = [\"pm25\", \"wind_forecast_kmph\", \"humidity_pct\", \"temp_c\"]\n",
    "\n",
    "# For a forecast the rows must NOT be shuffled. The model learns from the\n",
    "# first year and is tested on the second - exactly how it would be used:\n",
    "# trained on the past, judged on a future it has not seen.\n",
    "train = data[data[\"date\"].dt.year == 2024]\n",
    "test = data[data[\"date\"].dt.year == 2025]\n",
    "print(\"Training days (2024) :\", len(train))\n",
    "print(\"Testing days (2025)  :\", len(test))\n",
    "print()\n",
    "\n",
    "low = train[features].min()\n",
    "high = train[features].max()\n",
    "X_train = ((train[features] - low) / (high - low)).values\n",
    "X_test = ((test[features] - low) / (high - low)).values\n",
    "y_train = train[\"poor_tomorrow\"].values\n",
    "y_test = test[\"poor_tomorrow\"].values\n",
    "\n",
    "# The perceptron starts knowing nothing: every weight and the bias are 0.\n",
    "weights = np.zeros(len(features))\n",
    "bias = 0.0\n",
    "rate = 0.1          # how big each nudge is\n",
    "passes = 30         # how many times it goes through the training days\n",
    "\n",
    "# The days are visited in a shuffled order on every pass. In date order,\n",
    "# every pass would end on December's long run of Poor days, and that last\n",
    "# streak would drag all the weights towards \"always Poor\".\n",
    "shuffle = np.random.default_rng(1)\n",
    "\n",
    "mistakes_each_pass = []\n",
    "for p in range(passes):\n",
    "    mistakes = 0\n",
    "    for i in shuffle.permutation(len(X_train)):\n",
    "        total = np.dot(X_train[i], weights) + bias\n",
    "        if total > 0:\n",
    "            output = 1\n",
    "        else:\n",
    "            output = 0\n",
    "        error = y_train[i] - output          # +1, 0 or -1\n",
    "        if error != 0:\n",
    "            # Wrong: move every weight a little towards the right answer.\n",
    "            weights = weights + rate * error * X_train[i]\n",
    "            bias = bias + rate * error\n",
    "            mistakes = mistakes + 1\n",
    "    mistakes_each_pass.append(mistakes)\n",
    "\n",
    "print(\"Mistakes in the first pass :\", mistakes_each_pass[0])\n",
    "print(\"Mistakes in the last pass  :\", mistakes_each_pass[-1])\n",
    "print()\n",
    "print(\"Weights the perceptron learned:\")\n",
    "for i in range(len(features)):\n",
    "    print(\"  \" + features[i].ljust(20), round(weights[i], 3))\n",
    "print(\"  \" + \"bias\".ljust(20), round(bias, 3))\n",
    "print()\n",
    "\n",
    "# Chart 3: the perceptron learning.\n",
    "plt.figure(figsize=(7, 4))\n",
    "plt.plot(range(1, passes + 1), mistakes_each_pass, marker=\"o\", color=\"#4e79a7\", markersize=4)\n",
    "plt.title(\"The perceptron learning: mistakes in each pass\")\n",
    "plt.xlabel(\"Pass through the training days\")\n",
    "plt.ylabel(\"Mistakes\")\n",
    "plt.grid(True, linestyle=\"--\", alpha=0.4)\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart3_learning.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 4: the weights it learned.\n",
    "plt.figure(figsize=(7, 4))\n",
    "colours = []\n",
    "for w in weights:\n",
    "    if w > 0:\n",
    "        colours.append(\"#e15759\")\n",
    "    else:\n",
    "        colours.append(\"#59a14f\")\n",
    "plt.barh(features, weights, color=colours)\n",
    "plt.axvline(0, color=\"#999999\")\n",
    "plt.title(\"What the perceptron learned (red pushes towards Poor)\")\n",
    "plt.xlabel(\"Weight\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart4_weights.png\")\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Stage 5 - Evaluation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Stage 5: Evaluation ---\")\n",
    "perceptron = []\n",
    "for i in range(len(X_test)):\n",
    "    if np.dot(X_test[i], weights) + bias > 0:\n",
    "        perceptron.append(1)\n",
    "    else:\n",
    "        perceptron.append(0)\n",
    "\n",
    "# A simple baseline to beat: \"tomorrow will be like today\".\n",
    "same_as_today = []\n",
    "for v in test[\"pm25\"]:\n",
    "    if v > 90:\n",
    "        same_as_today.append(1)\n",
    "    else:\n",
    "        same_as_today.append(0)\n",
    "\n",
    "results = []\n",
    "for name, predicted in [[\"Same as today\", same_as_today], [\"Perceptron\", perceptron]]:\n",
    "    tp = fn = fp = tn = 0\n",
    "    for i in range(len(y_test)):\n",
    "        if y_test[i] == 1 and predicted[i] == 1:\n",
    "            tp = tp + 1\n",
    "        elif y_test[i] == 1:\n",
    "            fn = fn + 1\n",
    "        elif predicted[i] == 1:\n",
    "            fp = fp + 1\n",
    "        else:\n",
    "            tn = tn + 1\n",
    "    accuracy = (tp + tn) / len(y_test) * 100\n",
    "    precision = 0\n",
    "    if tp + fp > 0:\n",
    "        precision = tp / (tp + fp) * 100\n",
    "    recall = 0\n",
    "    if tp + fn > 0:\n",
    "        recall = tp / (tp + fn) * 100\n",
    "    f1 = 0\n",
    "    if precision + recall > 0:\n",
    "        f1 = 2 * precision * recall / (precision + recall)\n",
    "    results.append([name, accuracy, precision, recall, f1])\n",
    "    print(name)\n",
    "    print(\"  \" + name + \" TP, FN, FP, TN :\", tp, fn, fp, tn)\n",
    "    print(\"  \" + name + \" accuracy  :\", round(accuracy, 1), \"%\")\n",
    "    print(\"  \" + name + \" precision :\", round(precision, 1), \"%\")\n",
    "    print(\"  \" + name + \" recall    :\", round(recall, 1), \"%\")\n",
    "    print(\"  \" + name + \" F1 score  :\", round(f1, 1), \"%\")\n",
    "    print()\n",
    "\n",
    "# Chart 5: perceptron against the baseline.\n",
    "x = np.arange(4)\n",
    "plt.figure(figsize=(8, 4.5))\n",
    "plt.bar(x - 0.2, results[0][1:5], width=0.4, label=\"Same as today\", color=\"#9c9c9c\")\n",
    "plt.bar(x + 0.2, results[1][1:5], width=0.4, label=\"Perceptron\", color=\"#4e79a7\")\n",
    "plt.xticks(x, [\"Accuracy\", \"Precision\", \"Recall\", \"F1 score\"])\n",
    "plt.ylim(0, 100)\n",
    "plt.ylabel(\"Percent (2025, unseen)\")\n",
    "plt.title(\"Forecasting Poor air one day ahead\")\n",
    "plt.legend()\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart5_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",
    "today = pd.DataFrame({\n",
    "    \"pm25\":               [78, 120, 85],\n",
    "    \"wind_forecast_kmph\": [3.0, 18.0, 9.0],\n",
    "    \"humidity_pct\": [72, 45, 60],\n",
    "    \"temp_c\":       [15.0, 24.0, 19.0],\n",
    "})\n",
    "scaled = ((today - low) / (high - low)).values\n",
    "for i in range(len(today)):\n",
    "    total = np.dot(scaled[i], weights) + bias\n",
    "    if total > 0:\n",
    "        alert = \"ALERT: Poor air likely tomorrow - plan indoor activities\"\n",
    "    else:\n",
    "        alert = \"No alert\"\n",
    "    t = today.iloc[i]\n",
    "    print(\"PM2.5\", int(t[\"pm25\"]), \" wind forecast\", t[\"wind_forecast_kmph\"], \"km/h  humidity\", int(t[\"humidity_pct\"]),\n",
    "          \"%  ->\", alert)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
