{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0324628f",
   "metadata": {},
   "source": [
    "# 16 — Robust drift estimation (Huber)\n",
    "\n",
    "Doc page: [robust_drift.rst](../../docs/source/algorithms/robust_drift.rst).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c4c76fee",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:06:05.154949Z",
     "iopub.status.busy": "2026-05-12T14:06:05.154589Z",
     "iopub.status.idle": "2026-05-12T14:06:05.740238Z",
     "shell.execute_reply": "2026-05-12T14:06:05.738386Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from optimizr import _core as opt\n",
    "plt.rcParams['figure.figsize'] = (8.5, 4.5)\n",
    "plt.rcParams['figure.dpi'] = 110\n",
    "plt.rcParams['axes.grid'] = True\n",
    "plt.rcParams['grid.alpha'] = 0.3\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1acf345b",
   "metadata": {},
   "source": [
    "## Cellule 1 — Récupération du drift d'un OU sous bruit gaussien\n",
    "\n",
    "**Théorème.** Pour le processus d'Ornstein–Uhlenbeck discret\n",
    "$$x_{k+1} = x_k + (a + b x_k)\\,\\Delta t + \\sigma\\,\\varepsilon_k,$$\n",
    "l'estimateur de Huber via IRLS converge vers $(a, b)$ pour bruit\n",
    "symétrique.\n",
    "\n",
    "**Équation pivot (lien avec OU continu).** $dX_t = (a + b X_t) dt\n",
    "+ \\sigma dW_t$ ; en posant $a = \\theta\\mu$, $b = -\\theta$, on\n",
    "récupère le mean-reversion $dX_t = \\theta(\\mu - X_t)dt + \\sigma dW_t$.\n",
    "\n",
    "**Ce que la cellule vérifie.** $\\theta = 2$, $\\mu = 1$ donc\n",
    "$a_\\text{vrai} = 2$, $b_\\text{vrai} = -2$.  L'estimateur retrouve\n",
    "ces valeurs sur $N = 1000$ observations.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c43c131e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:06:05.743817Z",
     "iopub.status.busy": "2026-05-12T14:06:05.743466Z",
     "iopub.status.idle": "2026-05-12T14:06:06.453430Z",
     "shell.execute_reply": "2026-05-12T14:06:06.452371Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a vrai / estimé : 2.000 / 2.296\n",
      "b vrai / estimé : -2.000 / -2.415\n",
      "theta_hat = 2.415,  mu_hat = 0.951\n",
      "itérations IRLS : 9\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 142292 characters]",
      "text/plain": [
       "<Figure size 1320x440 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "rng = np.random.default_rng(0)\n",
    "theta_true, mu_true, sigma = 2.0, 1.0, 0.2\n",
    "N = 1000\n",
    "dt = 0.01\n",
    "x = np.zeros(N)\n",
    "x[0] = 0.5\n",
    "for k in range(N - 1):\n",
    "    x[k + 1] = x[k] + theta_true * (mu_true - x[k]) * dt + \\\n",
    "               sigma * np.sqrt(dt) * rng.standard_normal()\n",
    "\n",
    "res = opt.robust_drift(x.tolist(), dt, huber_delta=1.345)\n",
    "a_hat, b_hat = res['a'], res['b']\n",
    "a_true, b_true = theta_true * mu_true, -theta_true\n",
    "print(f\"a vrai / estimé : {a_true:.3f} / {a_hat:.3f}\")\n",
    "print(f\"b vrai / estimé : {b_true:.3f} / {b_hat:.3f}\")\n",
    "print(f\"theta_hat = {-b_hat:.3f},  mu_hat = {a_hat / -b_hat:.3f}\")\n",
    "print(f\"itérations IRLS : {res['iterations']}\")\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n",
    "ts = np.arange(N) * dt\n",
    "axes[0].plot(ts, x, lw=0.8)\n",
    "axes[0].axhline(mu_true, ls='--', color='gray', label=fr'$\\mu = {mu_true}$')\n",
    "axes[0].set_xlabel('t'); axes[0].set_ylabel('x(t)')\n",
    "axes[0].set_title(\"Trajectoire OU simulée\")\n",
    "axes[0].legend()\n",
    "dx = (x[1:] - x[:-1]) / dt\n",
    "axes[1].scatter(x[:-1], dx, s=4, alpha=0.4, label='observations')\n",
    "xs_lin = np.linspace(x.min(), x.max(), 50)\n",
    "axes[1].plot(xs_lin, a_true + b_true * xs_lin, 'k--',\n",
    "             lw=1.5, label='vrai drift')\n",
    "axes[1].plot(xs_lin, a_hat + b_hat * xs_lin, 'C3-',\n",
    "             lw=2, label='Huber estimé')\n",
    "axes[1].set_xlabel('x'); axes[1].set_ylabel(r'$\\Delta x / \\Delta t$')\n",
    "axes[1].set_title(\"Régression robuste sur le drift\")\n",
    "axes[1].legend()\n",
    "fig.tight_layout(); plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1fabf161",
   "metadata": {},
   "source": [
    "**Résultat attendu.** $\\hat a \\approx 2.0$, $\\hat b \\approx -2.0$.\n",
    "\n",
    "**Lecture du graphique.** Gauche : trajectoire qui oscille autour de\n",
    "$\\mu = 1$.  Droite : nuage des incréments avec deux droites\n",
    "superposées (vraie et estimée).\n",
    "\n",
    "**Conclusion.** Sur bruit gaussien, Huber se comporte comme OLS\n",
    "(efficacité ~95 %).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bdb6e2dc",
   "metadata": {},
   "source": [
    "## Cellule 2 — Robustesse face à un bruit de Cauchy\n",
    "\n",
    "**Théorème (consistance de Huber sous queues lourdes).** Pour\n",
    "$\\varepsilon_k \\sim \\text{Cauchy}$, OLS *diverge* (variance\n",
    "infinie) tandis que Huber reste consistant.\n",
    "\n",
    "**Équation pivot.**\n",
    "$$\\hat\\theta_\\text{Huber} \\xrightarrow{a.s.} \\theta_0,\n",
    "   \\qquad \\hat\\theta_\\text{OLS} \\nrightarrow \\theta_0\n",
    "   \\text{ (Cauchy)}.$$\n",
    "\n",
    "**Ce que la cellule vérifie.** On compare Huber vs OLS sur 30 jeux\n",
    "synthétiques OU avec bruit de Cauchy injecté.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "f01bc011",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T14:06:06.458599Z",
     "iopub.status.busy": "2026-05-12T14:06:06.458229Z",
     "iopub.status.idle": "2026-05-12T14:06:07.284751Z",
     "shell.execute_reply": "2026-05-12T14:06:07.283487Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "OLS   : médiane = 1.278, max = 6.83\n",
      "Huber : médiane = 0.023, max = 0.19\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 38100 characters]",
      "text/plain": [
       "<Figure size 1210x440 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "rng = np.random.default_rng(123)\n",
    "theta_true, mu_true = 2.0, 1.0\n",
    "sigma_cauchy = 0.05\n",
    "N = 1000; dt = 0.01\n",
    "n_runs = 30\n",
    "a_true, b_true = theta_true * mu_true, -theta_true\n",
    "\n",
    "ols_errs, huber_errs = [], []\n",
    "for _ in range(n_runs):\n",
    "    x = np.zeros(N); x[0] = 0.5\n",
    "    for k in range(N - 1):\n",
    "        eps = rng.standard_cauchy() * sigma_cauchy\n",
    "        x[k + 1] = x[k] + theta_true * (mu_true - x[k]) * dt + eps * np.sqrt(dt)\n",
    "    Xmat = np.column_stack([np.ones(N - 1), x[:-1]])\n",
    "    yvec = (x[1:] - x[:-1]) / dt\n",
    "    th_ols, *_ = np.linalg.lstsq(Xmat, yvec, rcond=None)\n",
    "    r = opt.robust_drift(x.tolist(), dt, huber_delta=1.345)\n",
    "    th_h = np.array([r['a'], r['b']])\n",
    "    ols_errs.append(np.linalg.norm(th_ols - np.array([a_true, b_true])))\n",
    "    huber_errs.append(np.linalg.norm(th_h - np.array([a_true, b_true])))\n",
    "\n",
    "ols_errs = np.array(ols_errs); huber_errs = np.array(huber_errs)\n",
    "print(f\"OLS   : médiane = {np.median(ols_errs):.3f}, max = {ols_errs.max():.2f}\")\n",
    "print(f\"Huber : médiane = {np.median(huber_errs):.3f}, max = {huber_errs.max():.2f}\")\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n",
    "axes[0].boxplot([ols_errs, huber_errs], labels=['OLS', 'Huber'])\n",
    "axes[0].set_ylabel(r'$\\|\\hat\\theta - \\theta_0\\|$')\n",
    "axes[0].set_yscale('log')\n",
    "axes[0].set_title(f\"{n_runs} runs OU + bruit Cauchy\")\n",
    "axes[1].hist(np.clip(ols_errs, 0, 10), bins=20,\n",
    "             alpha=0.5, label='OLS', color='C3')\n",
    "axes[1].hist(huber_errs, bins=20, alpha=0.7, label='Huber', color='C0')\n",
    "axes[1].set_xlabel(r'$\\|\\hat\\theta - \\theta_0\\|$')\n",
    "axes[1].set_ylabel('fréquence')\n",
    "axes[1].set_title(\"Distribution des erreurs\")\n",
    "axes[1].legend()\n",
    "fig.tight_layout(); plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "276c9e2d",
   "metadata": {},
   "source": [
    "**Résultat attendu.** Huber médiane bien inférieure à OLS médiane.\n",
    "\n",
    "**Lecture du graphique.** Gauche : boxplots log, Huber serré.\n",
    "Droite : histogramme — OLS dispersé, Huber concentré près de $0$.\n",
    "\n",
    "**Conclusion.** L'estimateur robuste est indispensable dès que les\n",
    "queues s'éloignent du gaussien (mesures expérimentales bruitées,\n",
    "signaux de capteurs aberrants).\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "rhftlab",
   "language": "python",
   "name": "rhftlab"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
