{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# A Grievance Desk that Reads\n",
    "\n",
    "grievance_desk.py\n",
    "\n",
    "Capstone project: a grievance desk that reads each citizen's complaint and\n",
    "sends it to the right department, so it is not left waiting in a pile.\n",
    "\n",
    "CBSE Class XII, Artificial Intelligence (843), Part C - Capstone Project.\n",
    "SDG 16 (Peace, Justice and Strong Institutions).\n",
    "\n",
    "Data Science Methodology, 10 steps. NATURAL LANGUAGE PROCESSING: the text\n",
    "is cleaned, turned into numbers with TF-IDF, and classified into six\n",
    "departments. Naive Bayes is compared with Logistic Regression, with\n",
    "stratified 5-fold cross validation. Grievances the model is unsure about\n",
    "go to a human officer.\n",
    "\n",
    "Libraries: pandas, numpy, matplotlib, scikit-learn, joblib."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 1 - Business Understanding"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"=\" * 64)\n",
    "print(\"THE GRIEVANCE DESK\")\n",
    "print(\"=\" * 64)\n",
    "print(\"Problem : grievances wait for days before someone reads them and\")\n",
    "print(\"          forwards them to the right department\")\n",
    "print(\"Goal    : read each grievance and route it at once; send the\")\n",
    "print(\"          unclear ones to a human officer\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 2 - Analytic Approach"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Analytic approach : predictive - text classification into 6 departments\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 3 - Data Requirements"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import string\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer\n",
    "from sklearn.naive_bayes import MultinomialNB\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.pipeline import make_pipeline\n",
    "from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score\n",
    "from sklearn.metrics import accuracy_score, confusion_matrix, precision_score, recall_score, f1_score\n",
    "import joblib"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 4 - Data Collection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data = pd.read_csv(\"grievances.csv\", keep_default_na=False)\n",
    "print(\"Grievances collected :\", len(data))\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 5 - Data Understanding"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 5: Data Understanding ---\")\n",
    "print(\"Department names as typed:\")\n",
    "print(data[\"department\"].value_counts().to_string())\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 6 - Data Preparation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 6: Data Preparation ---\")\n",
    "before = len(data)\n",
    "data = data.drop_duplicates(subset=\"id\")\n",
    "print(\"Grievances submitted twice, removed :\", before - len(data))\n",
    "blank = data[\"grievance\"].str.strip() == \"\"\n",
    "print(\"Blank grievances removed            :\", int(blank.sum()))\n",
    "data = data[~blank].copy()\n",
    "data[\"department\"] = data[\"department\"].str.strip().str.title()\n",
    "print(\"Department names after fixing case  :\", data[\"department\"].nunique())\n",
    "\n",
    "STOP = {\"a\", \"an\", \"the\", \"in\", \"on\", \"at\", \"of\", \"to\", \"is\", \"are\", \"and\", \"for\", \"from\", \"near\",\n",
    "        \"my\", \"our\", \"this\", \"it\", \"by\", \"has\", \"was\", \"very\", \"please\", \"kindly\", \"since\",\n",
    "        \"so\", \"no\", \"not\", \"we\", \"nobody\", \"again\", \"every\", \"last\", \"take\", \"action\", \"help\",\n",
    "        \"soon\", \"solve\", \"urgent\", \"request\", \"quick\", \"listening\", \"be\", \"days\", \"week\", \"month\"}\n",
    "\n",
    "\n",
    "def clean(text):\n",
    "    text = text.lower()\n",
    "    for mark in string.punctuation:\n",
    "        text = text.replace(mark, \" \")\n",
    "    words = [w for w in text.split() if w not in STOP and not w.isdigit()]\n",
    "    return \" \".join(words)\n",
    "\n",
    "\n",
    "data[\"clean\"] = data[\"grievance\"].apply(clean)\n",
    "print(\"Example before cleaning :\", data[\"grievance\"].iloc[2])\n",
    "print(\"Example after cleaning  :\", data[\"clean\"].iloc[2])\n",
    "data[\"words\"] = data[\"clean\"].str.split().str.len()\n",
    "print(\"Average words per grievance after cleaning :\", round(data[\"words\"].mean(), 1))\n",
    "print()\n",
    "\n",
    "depts = sorted(data[\"department\"].unique())\n",
    "counts = data[\"department\"].value_counts().reindex(depts)\n",
    "print(\"Grievances per department:\")\n",
    "print(counts.to_string())\n",
    "print()\n",
    "\n",
    "# Chart 1: grievances per department.\n",
    "plt.figure(figsize=(8, 4))\n",
    "plt.bar(depts, counts.values, color=\"#4e79a7\")\n",
    "plt.title(\"Grievances received by each department\")\n",
    "plt.ylabel(\"Grievances\")\n",
    "plt.xticks(rotation=20)\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart1_departments.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 2: how long the grievances are.\n",
    "plt.figure(figsize=(7, 4))\n",
    "plt.hist(data[\"words\"], bins=range(1, data[\"words\"].max() + 2), color=\"#59a14f\", edgecolor=\"white\")\n",
    "plt.title(\"Most grievances are short: words left after cleaning\")\n",
    "plt.xlabel(\"Words\")\n",
    "plt.ylabel(\"Grievances\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart2_lengths.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 3: the commonest words in each department.\n",
    "plt.figure(figsize=(11, 6))\n",
    "for k, d in enumerate(depts):\n",
    "    words = \" \".join(data[data[\"department\"] == d][\"clean\"]).split()\n",
    "    top = pd.Series(words).value_counts().head(6)\n",
    "    plt.subplot(2, 3, k + 1)\n",
    "    plt.barh(top.index[::-1], top.values[::-1], color=\"#f28e2b\")\n",
    "    plt.title(d)\n",
    "plt.suptitle(\"The commonest words in each department's grievances\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart3_top_words.png\")\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 7 - Modelling"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 7: Modelling ---\")\n",
    "X_train, X_test, y_train, y_test = train_test_split(\n",
    "    data[\"clean\"], data[\"department\"], test_size=0.2, stratify=data[\"department\"], random_state=4)\n",
    "print(\"Training grievances :\", len(X_train))\n",
    "print(\"Testing grievances  :\", len(X_test))\n",
    "bayes = make_pipeline(TfidfVectorizer(), MultinomialNB())\n",
    "logistic = make_pipeline(TfidfVectorizer(), LogisticRegression(max_iter=2000))\n",
    "bayes.fit(X_train, y_train)\n",
    "logistic.fit(X_train, y_train)\n",
    "vocab = bayes.named_steps[\"tfidfvectorizer\"].get_feature_names_out()\n",
    "print(\"Vocabulary learned (distinct words) :\", len(vocab))\n",
    "example = bayes.named_steps[\"tfidfvectorizer\"].transform([X_train.iloc[0]])\n",
    "print(\"TF-IDF example :\", X_train.iloc[0])\n",
    "for j in example.nonzero()[1]:\n",
    "    print(\"   \", vocab[j], \":\", round(example[0, j], 3))\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 8 - Evaluation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 8: Evaluation ---\")\n",
    "pred_b = bayes.predict(X_test)\n",
    "pred_l = logistic.predict(X_test)\n",
    "print(\"Accuracy, Naive Bayes         :\", round(accuracy_score(y_test, pred_b), 4))\n",
    "print(\"Accuracy, Logistic Regression :\", round(accuracy_score(y_test, pred_l), 4))\n",
    "print(\"Macro F1, Naive Bayes         :\", round(f1_score(y_test, pred_b, average=\"macro\"), 4))\n",
    "print(\"Macro F1, Logistic Regression :\", round(f1_score(y_test, pred_l, average=\"macro\"), 4))\n",
    "folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)\n",
    "cv_b = cross_val_score(make_pipeline(TfidfVectorizer(), MultinomialNB()), data[\"clean\"], data[\"department\"],\n",
    "                       cv=folds, scoring=\"f1_macro\")\n",
    "cv_l = cross_val_score(make_pipeline(TfidfVectorizer(), LogisticRegression(max_iter=2000)), data[\"clean\"],\n",
    "                       data[\"department\"], cv=folds, scoring=\"f1_macro\")\n",
    "print(\"Macro F1 in each fold, Naive Bayes         :\", np.round(cv_b, 3))\n",
    "print(\"Macro F1 in each fold, Logistic Regression :\", np.round(cv_l, 3))\n",
    "print(\"Mean CV macro F1, Naive Bayes         :\", round(cv_b.mean(), 4))\n",
    "print(\"Mean CV macro F1, Logistic Regression :\", round(cv_l.mean(), 4))\n",
    "best_name = \"Logistic Regression\" if cv_l.mean() >= cv_b.mean() else \"Naive Bayes\"\n",
    "best = logistic if best_name == \"Logistic Regression\" else bayes\n",
    "pred = pred_l if best_name == \"Logistic Regression\" else pred_b\n",
    "print(\"Chosen model :\", best_name)\n",
    "print()\n",
    "\n",
    "prec = precision_score(y_test, pred, labels=depts, average=None)\n",
    "rec = recall_score(y_test, pred, labels=depts, average=None)\n",
    "print(\"Department      precision  recall\")\n",
    "for d, p, r in zip(depts, prec, rec):\n",
    "    print(f\"{d:<15} {p:9.3f} {r:7.3f}\")\n",
    "print()\n",
    "wrong = [i for i in range(len(y_test)) if pred[i] != y_test.iloc[i]]\n",
    "print(\"Grievances sent to the wrong department (test set) :\", len(wrong))\n",
    "print(\"The first eight:\")\n",
    "for i in wrong[:8]:\n",
    "    print(\"  \", repr(data.loc[y_test.index[i], \"grievance\"]), \"| true:\", y_test.iloc[i], \"| sent to:\", pred[i])\n",
    "print()\n",
    "\n",
    "# Chart 4: the confusion matrix.\n",
    "cm = confusion_matrix(y_test, pred, labels=depts)\n",
    "plt.figure(figsize=(7, 6))\n",
    "plt.imshow(cm, cmap=\"Blues\")\n",
    "for a in range(len(depts)):\n",
    "    for b in range(len(depts)):\n",
    "        if cm[a, b] > 0:\n",
    "            plt.text(b, a, cm[a, b], ha=\"center\", va=\"center\",\n",
    "                     color=\"white\" if cm[a, b] > cm.max() / 2 else \"black\")\n",
    "plt.xticks(range(len(depts)), depts, rotation=30)\n",
    "plt.yticks(range(len(depts)), depts)\n",
    "plt.xlabel(\"Sent to\")\n",
    "plt.ylabel(\"True department\")\n",
    "plt.title(best_name + \": confusion matrix (test set)\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart4_confusion.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 5: precision and recall for each department.\n",
    "x = np.arange(len(depts))\n",
    "plt.figure(figsize=(8.5, 4))\n",
    "plt.bar(x - 0.2, prec, width=0.4, label=\"Precision\", color=\"#4e79a7\")\n",
    "plt.bar(x + 0.2, rec, width=0.4, label=\"Recall\", color=\"#e15759\")\n",
    "plt.xticks(x, depts, rotation=20)\n",
    "plt.ylim(0.8, 1.01)\n",
    "plt.title(\"Precision and recall for each department\")\n",
    "plt.legend(loc=\"lower right\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart5_per_department.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 6: macro F1 in each fold.\n",
    "x = np.arange(5)\n",
    "plt.figure(figsize=(7.5, 4))\n",
    "plt.bar(x - 0.2, cv_b, width=0.4, label=\"Naive Bayes\", color=\"#9c9c9c\")\n",
    "plt.bar(x + 0.2, cv_l, width=0.4, label=\"Logistic Regression\", color=\"#76b7b2\")\n",
    "plt.xticks(x, [\"Fold 1\", \"Fold 2\", \"Fold 3\", \"Fold 4\", \"Fold 5\"])\n",
    "plt.ylim(0.8, 1.0)\n",
    "plt.ylabel(\"Macro F1\")\n",
    "plt.title(\"Stratified 5-fold cross validation\")\n",
    "plt.legend(loc=\"lower right\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart6_kfold.png\")\n",
    "plt.close()\n",
    "\n",
    "# How sure is the model? Its highest probability for each test grievance.\n",
    "sure = best.predict_proba(X_test).max(axis=1)\n",
    "right = pred == y_test.values\n",
    "print(\"Average confidence when right :\", round(sure[right].mean(), 3))\n",
    "if (~right).sum() > 0:\n",
    "    print(\"Average confidence when wrong :\", round(sure[~right].mean(), 3))\n",
    "else:\n",
    "    print(\"Average confidence when wrong : none were wrong\")\n",
    "THRESHOLD = 0.5\n",
    "print(\"Test grievances below\", THRESHOLD, \"confidence :\", int((sure < THRESHOLD).sum()))\n",
    "print(\"Wrong routings caught by the threshold :\", int((~right & (sure < THRESHOLD)).sum()), \"of\", int((~right).sum()))\n",
    "auto_test = sure >= THRESHOLD\n",
    "print(\"Accuracy of the automatic routings :\", round(right[auto_test].mean(), 4))\n",
    "print()\n",
    "\n",
    "# Chart 7: confidence of right and wrong routings.\n",
    "plt.figure(figsize=(7.5, 4))\n",
    "plt.hist([sure[right], sure[~right]], bins=np.linspace(0, 1, 21), stacked=True,\n",
    "         color=[\"#59a14f\", \"#e15759\"], label=[\"Routed correctly\", \"Routed wrongly\"])\n",
    "plt.axvline(THRESHOLD, color=\"black\", linestyle=\"--\", label=\"Send to an officer below this\")\n",
    "plt.title(\"How sure the model was about each test grievance\")\n",
    "plt.xlabel(\"Highest probability\")\n",
    "plt.ylabel(\"Grievances\")\n",
    "plt.legend(loc=\"upper left\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart7_confidence.png\")\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 9 - Deployment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 9: Deployment ---\")\n",
    "joblib.dump(best, \"grievance_model.pkl\")\n",
    "model = joblib.load(\"grievance_model.pkl\")\n",
    "\n",
    "\n",
    "def route(text):\n",
    "    cleaned = clean(text)\n",
    "    probs = model.predict_proba([cleaned])[0]\n",
    "    top = probs.argmax()\n",
    "    if probs[top] < THRESHOLD:\n",
    "        return \"Human officer\", probs[top]\n",
    "    return model.classes_[top], probs[top]\n",
    "\n",
    "\n",
    "for text in [\"The street light outside my house is not working for a week\",\n",
    "             \"Garbage is lying on the road and the drain is blocked\",\n",
    "             \"My pension has not come for two months\"]:\n",
    "    dept, p = route(text)\n",
    "    print(repr(text), \"->\", dept, \"(confidence\", str(round(p, 2)) + \")\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 10 - Feedback"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 10: Feedback ---\")\n",
    "new = pd.read_csv(\"new_month.csv\", keep_default_na=False)\n",
    "print(\"Grievances in the new month :\", len(new))\n",
    "sent = [route(t)[0] for t in new[\"grievance\"]]\n",
    "new[\"sent_to\"] = sent\n",
    "known = new[\"department\"].isin(model.classes_)\n",
    "auto = new[\"sent_to\"] != \"Human officer\"\n",
    "print(\"Routed automatically        :\", int(auto.sum()))\n",
    "print(\"Sent to a human officer     :\", int((~auto).sum()))\n",
    "print(\"Automatic routings correct  :\", int((new[\"sent_to\"] == new[\"department\"])[auto].sum()), \"of\", int(auto.sum()))\n",
    "print(\"Departments never seen in training :\", sorted(new.loc[~known, \"department\"].unique()))\n",
    "print(\"Of these grievances, sent to an officer :\", int((~auto & ~known).sum()), \"of\", int((~known).sum()))\n",
    "for i in new.index[~known]:\n",
    "    print(\"  \", repr(new.loc[i, \"grievance\"]), \"->\", new.loc[i, \"sent_to\"])\n",
    "\n",
    "# Chart 8: what happened to the new month's grievances.\n",
    "outcome = pd.Series(np.where(~auto, \"Sent to an officer\",\n",
    "                    np.where(new[\"sent_to\"] == new[\"department\"], \"Routed correctly\", \"Routed wrongly\")))\n",
    "outcome = outcome.value_counts().reindex([\"Routed correctly\", \"Routed wrongly\", \"Sent to an officer\"], fill_value=0)\n",
    "plt.figure(figsize=(7, 4))\n",
    "plt.bar(outcome.index, outcome.values, color=[\"#59a14f\", \"#e15759\", \"#edc948\"])\n",
    "for k, v in enumerate(outcome.values):\n",
    "    plt.text(k, v + 2, str(v), ha=\"center\")\n",
    "plt.title(\"Feedback: the new month's grievances\")\n",
    "plt.ylabel(\"Grievances\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart8_new_month.png\")\n",
    "plt.close()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
