{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Where to add pathway capacity — analysis notebook\n",
    "\n",
    "*Protection referral pathway performance · project deliverable*\n",
    "\n",
    "Cassion · data-analysis.cassion.dev\n",
    "\n",
    "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/thecassion/cassion-learning-platform/blob/main/apps/data-analysis/public/projects/protection-referral-pathway/notebooks/referral-pathway.python.en.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Before anything else\n",
    "\n",
    "This is synthetic protection and GBV data. No real person is described, and this\n",
    "file must never be a template for storing real case data — the safe version of\n",
    "that is a consent-governed case management system.\n",
    "\n",
    "What the dataset deliberately omits is part of the deliverable: no names, no\n",
    "contact details, no free text, no incident date, no location below admin2, no\n",
    "exact age, no incident type. None are needed to measure whether a pathway works,\n",
    "and under GBV information management principles the incident-level fields never\n",
    "leave the case management agency.\n",
    "\n",
    "## The decision this produces\n",
    "\n",
    "Which service line and which area receive additional capacity, and whether the\n",
    "disability gap needs a separate response.\n",
    "\n",
    "The problem the project exists to fix: the pathway was reported as a single\n",
    "completion rate, which named nothing actionable.\n",
    "\n",
    "Audience: the protection cluster coordinator and the GBV sub-cluster.\n",
    "\n",
    "## Setup"
   ],
   "id": "cell-001"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "URL = (\n",
    "    \"https://data-analysis.cassion.dev/datasets/files/\"\n",
    "    \"protection-referrals-2024.v1.csv\"\n",
    ")\n",
    "\n",
    "MIN_CELL = 20   # suppression threshold, set with the case management agency\n",
    "\n",
    "cases = pd.read_csv(URL, dtype={\"case_id\": \"string\"})\n",
    "print(f\"{len(cases)} cases\")"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Consent gates the denominator\n",
    "\n",
    "Completion is measured over cases that **consented** to referral. Counting a\n",
    "non-consenting case as a pathway failure both misstates performance and\n",
    "misrepresents a person's decision — the pathway did what it should when someone\n",
    "declined."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "DISABILITY = {\"true\": True, \"yes\": True, \"false\": False, \"no\": False}\n",
    "cases[\"disability\"] = (\n",
    "    cases[\"disability_reported\"].astype(\"string\").str.strip().str.lower().map(DISABILITY)\n",
    ")\n",
    "\n",
    "consenting = cases[cases[\"consent_to_refer\"]].copy()\n",
    "\n",
    "pd.Series({\n",
    "    \"cases\": len(cases),\n",
    "    \"consented\": int(cases[\"consent_to_refer\"].sum()),\n",
    "    \"consent rate\": round(cases[\"consent_to_refer\"].mean(), 3),\n",
    "    \"reached a service\": int(consenting[\"referral_accepted\"].sum()),\n",
    "    \"completion (consent-gated)\": round(consenting[\"referral_accepted\"].mean(), 3),\n",
    "    \"completion if all cases counted\": round(cases[\"referral_accepted\"].mean(), 3),\n",
    "})"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The two completion figures differ by five points, and the wrong one blames the\n",
    "pathway for people who chose not to be referred.\n",
    "\n",
    "## Contradictions resolved before anything is ranked"
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "timed = cases[\"days_to_first_service\"].notna()\n",
    "\n",
    "contradictions = pd.Series({\n",
    "    \"service time recorded, referral not accepted\": int((~cases[\"referral_accepted\"] & timed).sum()),\n",
    "    \"service time recorded, no referral made\": int((~cases[\"referral_made\"] & timed).sum()),\n",
    "    \"accepted, no service time recorded\": int((cases[\"referral_accepted\"] & ~timed).sum()),\n",
    "})\n",
    "contradictions"
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Flagged, not dropped: in case management a contradictory record is an entry issue\n",
    "to return to the caseworker, and deleting it destroys the only trace the case\n",
    "existed. The forty accepted referrals with no service time mean the **timeliness\n",
    "denominator is smaller than the completion denominator**, so the two are reported\n",
    "separately below.\n",
    "\n",
    "## Where the pathway breaks"
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def completion(df, by):\n",
    "    out = df.groupby(by).agg(\n",
    "        cases=(\"referral_accepted\", \"size\"),\n",
    "        completed=(\"referral_accepted\", \"sum\"),\n",
    "    )\n",
    "    out[\"completion\"] = (out[\"completed\"] / out[\"cases\"]).round(3)\n",
    "    return out.sort_values(\"completion\")\n",
    "\n",
    "completion(consenting, \"service_requested\")"
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "completion(consenting, \"admin2\")"
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Livelihood support completes at roughly a third the rate of health. That is not\n",
    "caseworker performance — it is which services exist and have capacity, which is\n",
    "exactly what a capacity decision should act on."
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "grid = pd.crosstab(\n",
    "    consenting[\"admin2\"], consenting[\"service_requested\"],\n",
    "    values=consenting[\"referral_accepted\"], aggfunc=\"mean\",\n",
    ").round(2)\n",
    "counts = pd.crosstab(consenting[\"admin2\"], consenting[\"service_requested\"])\n",
    "\n",
    "grid.where(counts >= MIN_CELL)"
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cells below the suppression threshold are blanked. In protection work a small\n",
    "cell is a disclosure risk as much as a statistical one, and the threshold is a\n",
    "decision taken with the case management agency rather than a formatting choice.\n",
    "\n",
    "## The recommendation\n",
    "\n",
    "Capacity goes where the completion rate is low **and** the volume is large enough\n",
    "for the gain to matter. A service line completing at 23% on 30 cases is a\n",
    "different problem from one completing at 23% on 200."
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lines = completion(consenting, \"service_requested\")\n",
    "lines[\"shortfall\"] = (\n",
    "    (lines[\"completion\"].max() - lines[\"completion\"]) * lines[\"cases\"]\n",
    ").round(0)\n",
    "lines.sort_values(\"shortfall\", ascending=False)"
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `shortfall` column is the number of additional people who would reach a\n",
    "service if that line performed like the best-performing one. It is a crude\n",
    "estimate — it assumes the gap is capacity rather than need — but it is the right\n",
    "shape for a capacity decision, and it ranks differently from completion alone."
   ],
   "id": "cell-014"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "areas = completion(consenting, \"admin2\")\n",
    "areas[\"shortfall\"] = (\n",
    "    (areas[\"completion\"].max() - areas[\"completion\"]) * areas[\"cases\"]\n",
    ").round(0)\n",
    "areas.sort_values(\"shortfall\", ascending=False)"
   ],
   "id": "cell-015"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The disability gap"
   ],
   "id": "cell-016"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gap = completion(consenting.dropna(subset=[\"disability\"]), \"disability\")\n",
    "gap"
   ],
   "id": "cell-017"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.stats import chi2_contingency\n",
    "\n",
    "known = consenting.dropna(subset=[\"disability\"])\n",
    "table = pd.crosstab(known[\"disability\"], known[\"referral_accepted\"])\n",
    "chi2, p, dof, expected = chi2_contingency(table)\n",
    "\n",
    "print(f\"completion gap: {gap['completion'].iloc[0] - gap['completion'].iloc[-1]:+.3f}\")\n",
    "print(f\"chi-square p  : {p:.2e}\")"
   ],
   "id": "cell-018"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "by_service = (\n",
    "    known.groupby([\"service_requested\", \"disability\"])[\"referral_accepted\"]\n",
    "    .agg([\"size\", \"mean\"])\n",
    "    .round(3)\n",
    "    .unstack()\n",
    ")\n",
    "by_service"
   ],
   "id": "cell-019"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Does the gap need a separate response?** The answer this notebook supports is\n",
    "yes, and for a reason the aggregate hides: the gap is not concentrated in one\n",
    "service line. It appears in four of the five — safety and security is the\n",
    "exception, at 35% either way — which means it is unlikely to be explained by the\n",
    "mix of services people with disabilities request. What it does not tell you is\n",
    "the mechanism — physical inaccessibility, pathways assuming mobility, or\n",
    "something else — and that is answered by asking caseworkers, not by this table.\n",
    "\n",
    "One area recorded disability as `Yes`/`No` rather than `true`/`false`. Without\n",
    "normalising, the disaggregation fragments into four categories, two of them from\n",
    "that single area and too small to interpret — so the gap would have been\n",
    "invisible.\n",
    "\n",
    "## What this does not establish\n",
    "\n",
    "Referral records show what the pathway did, not what people needed. A service\n",
    "line with few referrals may be one nobody needs or one nobody offers, and this\n",
    "dataset cannot separate the two.\n",
    "\n",
    "Timeliness is measured from referral to first service, not from incident, because\n",
    "the dataset holds no incident date and under GBV information management\n",
    "principles it should not. A survivor who reached a caseworker late and a service\n",
    "quickly appears here as fast."
   ],
   "id": "cell-020"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "file_extension": ".py"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
