{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# EP-M02 Companion Notebook\n",
        "## Making Trade-offs Under Risk Explicit\n",
        "\n",
        "**Asset:** EP-M02-A08  \n",
        "**Version:** v0.1  \n",
        "**Status:** G2 content prototype  \n",
        "\n",
        "This notebook supports auditability and sensitivity analysis. It is advisory and cannot authorize an institutional decision.\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Learning and decision contract\n",
        "\n",
        "The notebook will:\n",
        "- expose inputs and probability invariants;\n",
        "- compare expected value and expected utility;\n",
        "- calculate certainty equivalents and risk premiums;\n",
        "- evaluate information cost and delay;\n",
        "- test recommendation reversals;\n",
        "- keep hard constraints and subgroup effects visible;\n",
        "- export an executive summary for human review.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "import sys, platform, math, json, hashlib\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "import matplotlib.pyplot as plt\n",
        "SEED = 42\n",
        "rng = np.random.default_rng(SEED)\n",
        "ENVIRONMENT = {\"python\": sys.version.split()[0], \"platform\": platform.platform(), \"numpy\": np.__version__, \"pandas\": pd.__version__, \"seed\": SEED}\n",
        "ENVIRONMENT\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "def validate_probabilities(p, tol=1e-12):\n",
        "    p=np.asarray(p,dtype=float)\n",
        "    assert np.all(np.isfinite(p)), \"Probabilities must be finite\"\n",
        "    assert np.all(p>=0), \"Probabilities must be non-negative\"\n",
        "    assert abs(p.sum()-1.0)<=tol, \"Probabilities must sum to one\"\n",
        "    return True\n",
        "\n",
        "def linear_utility(x, lower, upper):\n",
        "    x=np.asarray(x,dtype=float)\n",
        "    if not upper>lower: raise ValueError(\"upper must exceed lower\")\n",
        "    return (x-lower)/(upper-lower)\n",
        "\n",
        "def exponential_utility(x, r, lower=0.0, upper=100.0):\n",
        "    x=np.asarray(x,dtype=float)\n",
        "    if r<0: raise ValueError(\"r must be non-negative\")\n",
        "    if r==0: return linear_utility(x,lower,upper)\n",
        "    return (1-np.exp(-r*(x-lower)))/(1-np.exp(-r*(upper-lower)))\n",
        "\n",
        "def expected_value(x,p): validate_probabilities(p); return float(np.dot(p,x))\n",
        "def expected_utility(x,p,u): validate_probabilities(p); return float(np.dot(p,u(x)))\n",
        "def certainty_equivalent_exponential(eu,r,lower=0.0,upper=100.0):\n",
        "    if not 0<=eu<=1: raise ValueError(\"normalized EU must lie in [0,1]\")\n",
        "    if r==0: return lower+eu*(upper-lower)\n",
        "    return lower-np.log(1-eu*(1-np.exp(-r*(upper-lower))))/r\n",
        "print(\"Core functions validated\")\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 1 Source prospect\n",
        "The source prospect uses outcomes `[0, 50, 100]` with probabilities `[0.20, 0.50, 0.30]`. Units and meanings must be supplied before institutional use.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "outcomes=np.array([0.,50.,100.])\n",
        "probabilities=np.array([.20,.50,.30])\n",
        "validate_probabilities(probabilities)\n",
        "prospect=pd.DataFrame({\"outcome\":outcomes,\"probability\":probabilities})\n",
        "prospect[\"p_times_x\"]=prospect.outcome*prospect.probability\n",
        "prospect\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "ev=expected_value(outcomes,probabilities)\n",
        "linear_eu=expected_utility(outcomes,probabilities,lambda x: linear_utility(x,0,100))\n",
        "{\"expected_value\":ev,\"expected_linear_utility\":linear_eu,\"probability_sum\":probabilities.sum()}\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 2 Risk-aversion comparison\n",
        "A normalized exponential utility curve is used only as an illustrative model. The risk parameter requires legitimate elicitation and approval.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "risk_parameters=[0.0,0.005,0.01,0.02,0.04]\n",
        "rows=[]\n",
        "for r in risk_parameters:\n",
        "    eu=expected_utility(outcomes,probabilities,lambda x,r=r: exponential_utility(x,r,0,100))\n",
        "    ce=certainty_equivalent_exponential(eu,r,0,100)\n",
        "    rows.append({\"risk_parameter\":r,\"expected_utility\":eu,\"certainty_equivalent\":ce,\"risk_premium\":ev-ce})\n",
        "risk_table=pd.DataFrame(rows)\n",
        "risk_table\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "grid=np.linspace(0,100,201)\n",
        "fig,ax=plt.subplots(figsize=(8,4.5))\n",
        "for r in [0,.01,.02,.04]: ax.plot(grid,exponential_utility(grid,r,0,100),label=f\"r={r}\")\n",
        "ax.set(xlabel=\"Outcome\",ylabel=\"Normalized utility\",title=\"Utility curvature and declared risk posture\")\n",
        "ax.legend(); ax.grid(alpha=.25); plt.show()\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 3 Alternatives and hard constraints\n",
        "The illustrative alternatives include subgroup consequences and a hard-safety flag. Infeasible alternatives are excluded before utility ranking.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "alternatives=pd.DataFrame([\n",
        "{\"alternative\":\"A high upside\",\"expected_money\":82,\"severe_loss_probability\":.08,\"vulnerable_group_impact\":-18,\"safety_pass\":False},\n",
        "{\"alternative\":\"B balanced\",\"expected_money\":74,\"severe_loss_probability\":.025,\"vulnerable_group_impact\":-4,\"safety_pass\":True},\n",
        "{\"alternative\":\"C cautious\",\"expected_money\":66,\"severe_loss_probability\":.01,\"vulnerable_group_impact\":-1,\"safety_pass\":True},\n",
        "])\n",
        "alternatives[\"feasible\"]=alternatives.safety_pass\n",
        "alternatives\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "feasible=alternatives.query(\"feasible\").copy()\n",
        "# Illustrative normalized score after feasibility; not a substitute for elicitation.\n",
        "feasible[\"illustrative_utility\"]=exponential_utility(feasible.expected_money.to_numpy(),.02,0,100)\n",
        "feasible.sort_values(\"illustrative_utility\",ascending=False)\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 4 Value of information\n",
        "Gross information value is the improvement from optimizing with information. Net value subtracts acquisition cost and delay cost.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "optimized_without_information=76.0\n",
        "optimized_with_information=84.0\n",
        "information_cost=5.0\n",
        "delay_cost=2.0\n",
        "gross_voi=optimized_with_information-optimized_without_information\n",
        "net_voi=gross_voi-information_cost-delay_cost\n",
        "voi={\"gross_value\":gross_voi,\"information_cost\":information_cost,\"delay_cost\":delay_cost,\"net_value\":net_voi,\"recommendation\":\"ACQUIRE\" if net_voi>0 else \"DO NOT ACQUIRE\"}\n",
        "voi\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 5 Sensitivity and recommendation reversals\n",
        "This test varies the risk parameter and severe-loss penalty. A result is fragile when small plausible changes reverse the preferred feasible alternative.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "sensitivity=[]\n",
        "for r in np.linspace(0,.04,9):\n",
        "    for penalty in [0,50,100,200,400]:\n",
        "        tmp=alternatives.query(\"feasible\").copy()\n",
        "        adjusted=tmp.expected_money-penalty*tmp.severe_loss_probability\n",
        "        tmp[\"score\"]=exponential_utility(adjusted.to_numpy(),r,lower=-100,upper=100)\n",
        "        winner=tmp.loc[tmp.score.idxmax(),\"alternative\"]\n",
        "        sensitivity.append({\"r\":round(float(r),4),\"severe_loss_penalty\":penalty,\"winner\":winner})\n",
        "sensitivity=pd.DataFrame(sensitivity)\n",
        "pd.crosstab(sensitivity.r,sensitivity.winner)\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "pivot=sensitivity.assign(win_code=lambda d:d.winner.map({\"B balanced\":1,\"C cautious\":2})).pivot(index=\"r\",columns=\"severe_loss_penalty\",values=\"win_code\")\n",
        "fig,ax=plt.subplots(figsize=(8,4.5)); im=ax.imshow(pivot.values,aspect=\"auto\",cmap=\"Blues\",vmin=1,vmax=2)\n",
        "ax.set_xticks(range(len(pivot.columns)),pivot.columns); ax.set_yticks(range(len(pivot.index)),pivot.index)\n",
        "ax.set(xlabel=\"Severe-loss penalty\",ylabel=\"Risk parameter r\",title=\"Recommendation regions (1=B, 2=C)\"); plt.colorbar(im,ax=ax); plt.show()\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 6 Distributional visibility\n",
        "Aggregate utility does not erase subgroup outcomes. The table below must remain in the evidence record even when an aggregate score is reported.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "distribution=alternatives[[\"alternative\",\"expected_money\",\"severe_loss_probability\",\"vulnerable_group_impact\",\"safety_pass\"]].copy()\n",
        "distribution[\"requires_escalation\"]=~distribution.safety_pass | (distribution.vulnerable_group_impact<=-10)\n",
        "distribution\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 7 Executive summary export and human gate\n",
        "The computational summary is evidence for review. The final status requires a named human decision owner.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "preferred=feasible.sort_values(\"illustrative_utility\",ascending=False).iloc[0][\"alternative\"]\n",
        "summary={\n",
        "\"module\":\"EP-M02\",\n",
        "\"asset\":\"EP-M02-A08\",\n",
        "\"source_expected_value\":ev,\n",
        "\"risk_parameter_example\":.02,\n",
        "\"preferred_feasible_alternative_example\":preferred,\n",
        "\"gross_information_value\":gross_voi,\n",
        "\"net_information_value\":net_voi,\n",
        "\"hard_constraints_applied\":True,\n",
        "\"sensitivity_tested\":True,\n",
        "\"human_decision_required\":True,\n",
        "\"allowed_decisions\":[\"ACCEPT\",\"REVISE\",\"ESCALATE\"]\n",
        "}\n",
        "summary[\"evidence_sha256\"]=hashlib.sha256(json.dumps(summary,sort_keys=True).encode()).hexdigest()\n",
        "summary\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Interpretation and limitations\n",
        "\n",
        "- All numerical values are instructional and must be replaced or justified for real use.\n",
        "- The exponential function is illustrative, not an institutionally approved preference model.\n",
        "- Hard constraints are applied before utility ranking.\n",
        "- Subgroup consequences remain visible after aggregation.\n",
        "- Sensitivity identifies conditional recommendation regions; it does not eliminate model risk.\n",
        "- Only a named human authority may issue **ACCEPT**, **REVISE**, or **ESCALATE**.\n"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}