{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "bdf377b8",
   "metadata": {},
   "source": [
    "# Analisi completa della molla 1\n",
    "Tutto quello che c'è da sapere sulla molla 1, possibilmente con la massima chiarezza."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 210,
   "id": "3a34cc3f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Heavy lifting\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from scipy import stats\n",
    "\n",
    "# Mostrare i dati\n",
    "import ipysheet\n",
    "from IPython.display import display\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "\n",
    "# Calcolo simbolico\n",
    "import sympy as sp\n",
    "\n",
    "\n",
    "G = 9.806\n",
    "uG = 0.004"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "960e68a5",
   "metadata": {},
   "source": [
    "## Import dei dati (criterio di Chauvenet)\n",
    "- Primo test di implementazione 1\n",
    "- Scrittura con logica chiara   2\n",
    "- Codice pulito per bene        3 (sono abbastanza soddisfatto)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 211,
   "id": "1e2fabea",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Blocco funzioni per Chauvenet\n",
    "# Probabilità\n",
    "def p_t_student(valori, err_strumentale):\n",
    "  n   = len(valori)\n",
    "  GdL = n - 1\n",
    "  if n <= 2:\n",
    "    return np.ones(n)\n",
    "\n",
    "  media = valori.mean()\n",
    "  s     = valori.std(ddof=1)\n",
    "  if s == 0:\n",
    "    return np.ones(n)\n",
    "\n",
    "  s = np.sqrt(s**2 + err_strumentale**2)\n",
    "  return 2 * (1 - stats.t.cdf(np.abs(valori - media) / s, df=GdL))\n",
    "\n",
    "# Indice del peggiore outlier o None\n",
    "def trova_outlier(valori, err_strumentale):\n",
    "  n       = len(valori)\n",
    "  soglia  = 1 / (2 * n)\n",
    "  p       = p_t_student(valori, err_strumentale)\n",
    "  idx_min = np.argmin(p)\n",
    "\n",
    "  if p[idx_min] < soglia:\n",
    "      return idx_min\n",
    "  return None\n",
    "\n",
    "# Rimuove outlier\n",
    "def rimuovi_outlier(valori, err_strumentale):\n",
    "  rimossi  = []\n",
    "  campione = valori.copy()\n",
    "\n",
    "  while len(campione) > 2:\n",
    "    idx = trova_outlier(campione, err_strumentale)\n",
    "\n",
    "    if idx is None: # nessun outlier: stop\n",
    "      break\n",
    "\n",
    "    rimossi.append(campione[idx])\n",
    "    campione = np.delete(campione, idx)\n",
    "\n",
    "  return campione, rimossi\n",
    "\n",
    "# return media u rimossi []\n",
    "def stats_riga(valori_raw, err_strumentale):\n",
    "  # caso 0\n",
    "  if len(valori_raw) == 0:\n",
    "    return {\"media\": np.nan, \"u\": np.nan, \"n\": 0, \"rimossi\": []}\n",
    "\n",
    "  # caso 1\n",
    "  if len(valori_raw) == 1:\n",
    "    return {\"media\": valori_raw[0], \"u\": err_strumentale, \"n\": 1, \"rimossi\": []}\n",
    "\n",
    "  # caso n>1\n",
    "  puliti, rimossi = rimuovi_outlier(valori_raw, err_strumentale)\n",
    "\n",
    "  media = puliti.mean()\n",
    "\n",
    "  s = puliti.std(ddof=1)\n",
    "  n = len(puliti)\n",
    "  s = s / np.sqrt(n)\n",
    "  s = np.sqrt(s**2 + err_strumentale**2)\n",
    "\n",
    "  return {\"media\": media, \"u\": s, \"n\": n, \"rimossi\": rimossi}\n",
    "\n",
    "# Calcola la statistica con Criterio di Chauvenet\n",
    "def calcola_stats(df, prefix, err_strumentale):\n",
    "  cols = [col for col in df.columns if col.startswith(prefix)]\n",
    "\n",
    "  for i, row in df.iterrows():\n",
    "    valori = row[cols].dropna().values.astype(float)\n",
    "    ans    = stats_riga(valori, err_strumentale)\n",
    "\n",
    "    df.at[i, prefix]         = ans[\"media\"]\n",
    "    df.at[i, f\"u{prefix}\"]   = ans[\"u\"]\n",
    "    df.at[i, f\"n{prefix}\"]   = int(ans[\"n\"])\n",
    "    df.at[i, f\"out{prefix}\"] = str(ans[\"rimossi\"])\n",
    "\n",
    "  return df\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 212,
   "id": "a5ff5ff5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "F  = g*m\n",
      "sigma_F = sqrt(g**2*sigma_m**2 + m**2*sigma_g**2)\n",
      "\n",
      "Formula LaTeX:\n",
      "F = g m\n",
      "sigma_F = \\sqrt{g^{2} \\sigma_{m}^{2} + m^{2} \\sigma_{g}^{2}}\n"
     ]
    }
   ],
   "source": [
    "# Blocco funzioni calcolo simbolico la forza\n",
    "# simboli\n",
    "m, x, g = sp.symbols('m x g', positive=True)\n",
    "sigma_m, sigma_x, sigma_g = sp.symbols('sigma_m sigma_x sigma_g', positive=True)\n",
    "\n",
    "F = m * g\n",
    "\n",
    "# derivate parziali\n",
    "dF_dm = sp.diff(F, m)\n",
    "dF_dg = sp.diff(F, g)\n",
    "\n",
    "sigma_F = sp.sqrt((dF_dm * sigma_m)**2 + (dF_dg * sigma_g)**2)\n",
    "\n",
    "print(\"F  =\", F)\n",
    "print(\"sigma_F =\", sigma_F)\n",
    "print(\"\\nFormula LaTeX:\")\n",
    "print(f\"F = {sp.latex(F)}\")\n",
    "print(f\"sigma_F = {sp.latex(sigma_F.simplify())}\")\n",
    "\n",
    "# funzioni numeriche\n",
    "F_fn  = sp.lambdify((m, g),  F,  'numpy')\n",
    "uF_fn = sp.lambdify((m, sigma_m, g, sigma_g), sigma_F, 'numpy')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 213,
   "id": "e649eddc",
   "metadata": {},
   "outputs": [],
   "source": [
    "dfCalibro = pd.read_csv(r'molla1Calibro.csv')\n",
    "\n",
    "# Costanti strumentali\n",
    "ERR_CALIBRO = 0.02 / np.sqrt(12)\n",
    "ERR_BILANCIA = 0.01 / np.sqrt(12)\n",
    "\n",
    "# Chauvenet e poi statistica di base\n",
    "dfCalibro = calcola_stats(dfCalibro, \"dx\", ERR_CALIBRO)\n",
    "dfCalibro = calcola_stats(dfCalibro, \"m\",  ERR_BILANCIA)\n",
    "\n",
    "# Aggiunta delle forze\n",
    "dfCalibro[\"F\"]  = F_fn(dfCalibro[\"m\"],  G)\n",
    "dfCalibro[\"uF\"] = uF_fn(dfCalibro[\"m\"], dfCalibro[\"um\"], G, uG)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 214,
   "id": "42ff21d6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "8e9d979d582d48549847d52a98f7dae6",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Sheet(cells=(Cell(column_end=0, column_start=0, row_end=4, row_start=0, squeeze_row=False, type='numeric', val…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "sheet = ipysheet.from_dataframe(dfCalibro)\n",
    "\n",
    "display(sheet)\n",
    "# dfCalibro.head(2).to_json(orient=\"records\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c58a6e26",
   "metadata": {},
   "source": [
    "## Regressione lineare Carpi dati del calibro\n",
    "chi² Come persone normali"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 215,
   "id": "dfab4331",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Blocco di funzione regressione Carpi\n",
    "def sigma_y_equiv(x, y, sigma_x, sigma_y):\n",
    "  # Stima iniziale di A con sola sigma_y\n",
    "  sy2 = sigma_y**2\n",
    "  Sw   = np.sum(1 / sy2)\n",
    "  Sx   = np.sum(x / sy2)\n",
    "  Sxx  = np.sum(x**2 / sy2)\n",
    "  Sy   = np.sum(y / sy2)\n",
    "  Sxy  = np.sum(x * y / sy2)\n",
    "  delta = Sxx * Sw - Sx**2\n",
    "  A_est = (Sxy * Sw - Sx * Sy) / delta\n",
    "\n",
    "  # Propagazione\n",
    "  sigma_eq = np.sqrt(sigma_y**2 + A_est**2 * sigma_x**2)\n",
    "  return sigma_eq\n",
    "\n",
    "\n",
    "def reg_lin(x, y, sigma_x, sigma_y):\n",
    "  x = np.asarray(x, dtype=float)\n",
    "  y = np.asarray(y, dtype=float)\n",
    "  sigma_x = np.asarray(sigma_x, dtype=float)\n",
    "  sigma_y = np.asarray(sigma_y, dtype=float)\n",
    "\n",
    "  # Propagazione errore x → y\n",
    "  sigma_y_eq = sigma_y_equiv(x, y, sigma_x, sigma_y)\n",
    "\n",
    "  # Somme pesate\n",
    "  w    = 1.0 / sigma_y_eq**2\n",
    "  Sw   = np.sum(w)\n",
    "  Sx   = np.sum(w * x)\n",
    "  Sxx  = np.sum(w * x**2)\n",
    "  Sy   = np.sum(w * y)\n",
    "  Sxy  = np.sum(w * x * y)\n",
    "  delta = Sxx * Sw - Sx**2\n",
    "\n",
    "  # Parametri\n",
    "  A      = (Sxy * Sw - Sx * Sy)  / delta\n",
    "  B      = (Sxx * Sy - Sxy * Sx) / delta\n",
    "  sigma_A = np.sqrt(Sw  / delta)\n",
    "  sigma_B = np.sqrt(Sxx / delta)\n",
    "  cov_AB  = -Sx / delta\n",
    "\n",
    "  # Chi quadro\n",
    "  x2  = np.sum((y - A * x - B)**2 / sigma_y_eq**2)\n",
    "  dof = len(x) - 2\n",
    "  P = stats.chi2.sf(x2, dof)\n",
    "\n",
    "  return A, B, sigma_A, sigma_B, cov_AB, x2, P"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 216,
   "id": "5d7ca0a0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Residuo:\n",
      "  r = x + B/A - y/A\n",
      "\n",
      "Formula LaTeX:\n",
      "  r       = x + \\frac{B}{A} - \\frac{y}{A}\n",
      "  sigma_r = \\sqrt{\\frac{A^{4} \\sigma_{x}^{2} + A^{2} \\left(\\sigma_{B}^{2} + \\sigma_{y}^{2}\\right) - 2 A cov_{AB} \\left(B - y\\right) + \\sigma_{A}^{2} \\left(B - y\\right)^{2}}{A^{4}}}\n"
     ]
    }
   ],
   "source": [
    "# Blocco calcolo simbolico residui\n",
    "A, B, x, y = sp.symbols('A B x y', real=True)\n",
    "sigma_A, sigma_B, sigma_x, sigma_y = sp.symbols('sigma_A sigma_B sigma_x sigma_y', positive=True)\n",
    "cov_AB = sp.symbols('cov_AB', real=True)\n",
    "\n",
    "# Residuo: r = x - (y/A - B/A)\n",
    "r = x - (y / A - B / A)\n",
    "\n",
    "# Propagazione errore (con covarianza A,B)\n",
    "dr_dA  = sp.diff(r, A)\n",
    "dr_dB  = sp.diff(r, B)\n",
    "dr_dx  = sp.diff(r, x)\n",
    "dr_dy  = sp.diff(r, y)\n",
    "\n",
    "sigma_r  = sp.sqrt(\n",
    "  (dr_dA * sigma_A)**2 +\n",
    "  (dr_dB * sigma_B)**2 +\n",
    "  (dr_dx * sigma_x)**2 +\n",
    "  (dr_dy * sigma_y)**2 +\n",
    "  2 * dr_dA * dr_dB * cov_AB\n",
    ")\n",
    "\n",
    "# Funzioni numeriche\n",
    "r_fn      = sp.lambdify((x , y , A , B ), r, 'numpy')\n",
    "sigma_r_fn = sp.lambdify(\n",
    "  (x , y , A , B , sigma_x , sigma_y , sigma_A , sigma_B , cov_AB ),\n",
    "  sigma_r , 'numpy'\n",
    ")\n",
    "\n",
    "print(\"Residuo:\")\n",
    "print(f\"  r = {r }\")\n",
    "print(f\"\\nFormula LaTeX:\")\n",
    "print(f\"  r       = {sp.latex(r)}\")\n",
    "print(f\"  sigma_r = {sp.latex(sp.simplify(sigma_r))}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 217,
   "id": "cd68b201",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Ax + B : \n",
      "AC      = 23.9437 ± 0.0424\n",
      "BC      = 2719.8777 ± 2.8546\n",
      "cov_ABC = 0.119914\n",
      "chi²    = 55.1017\n",
      "P(chi², ∞)= 0.0000\n"
     ]
    }
   ],
   "source": [
    "AC, BC, uAC, uBC, covABC, chiC, PC = reg_lin(-dfCalibro[\"dx\"], dfCalibro[\"F\"], dfCalibro[\"udx\"], dfCalibro[\"uF\"])\n",
    "print(\"Ax + B : \")\n",
    "print(f\"AC      = {AC:.4f} ± {uAC:.4f}\")\n",
    "print(f\"BC      = {BC:.4f} ± {uBC:.4f}\")\n",
    "print(f\"cov_ABC = {covABC:.6f}\")\n",
    "print(f\"chi²    = {chiC:.4f}\")\n",
    "print(f\"P(chi², ∞)= {PC:.4f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 218,
   "id": "f98a3612",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Calcolo numerico residui sul dfCalibro\n",
    "dfCalibro[\"r\"] = r_fn(\n",
    "  -dfCalibro[\"dx\"],\n",
    "  dfCalibro[\"F\"],\n",
    "  AC,\n",
    "  BC\n",
    ")\n",
    "\n",
    "dfCalibro[\"ur\"] = sigma_r_fn(\n",
    "  -dfCalibro[\"dx\"], dfCalibro[\"F\"],\n",
    "  AC, BC,\n",
    "  dfCalibro[\"udx\"], dfCalibro[\"uF\"],\n",
    "  uAC, uBC, covABC\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5de53d0e",
   "metadata": {},
   "source": [
    "### Grafici"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 219,
   "id": "68eb66dc",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 58412 characters]",
      "text/plain": [
       "<Figure size 800x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Fit\n",
    "SCALA = 10\n",
    "fig, ax = plt.subplots(figsize=(8, 5))\n",
    "\n",
    "x_data = -dfCalibro[\"dx\"]\n",
    "y_data =  dfCalibro[\"F\"]\n",
    "\n",
    "x_fit = np.linspace(x_data.min(), x_data.max(), 300)\n",
    "y_fit = AC * x_fit + BC\n",
    "\n",
    "ax.errorbar(\n",
    "    x_data, y_data,\n",
    "    xerr=SCALA * dfCalibro[\"udx\"], yerr=SCALA * dfCalibro[\"uF\"],\n",
    "    fmt='o', color=sns.color_palette()[0],\n",
    "    ecolor='gray', elinewidth=1, capsize=3,\n",
    "    markersize=5, label=f\"Dati (barre errore x{SCALA})\"\n",
    ")\n",
    "ax.plot(\n",
    "    x_fit, y_fit,\n",
    "    color='red', linewidth=1.5,\n",
    "    label=f\"Fit: $A={AC:.3f}\\\\pm{uAC:.3f}$\\n\"\n",
    "          f\"     $B={BC:.2f}\\\\pm{uBC:.2f}$\\n\"\n",
    ")\n",
    "\n",
    "sns.despine(ax=ax)\n",
    "ax.set_xlabel(r\"$- x$  [mm]\")\n",
    "ax.set_ylabel(\"F  [mN]\")\n",
    "ax.set_title(\"Fit lineare — Molla 1 (calibro)\")\n",
    "ax.legend(fontsize=9)\n",
    "ax.grid(True, linestyle=':', linewidth=0.5, alpha=0.7)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 220,
   "id": "831a4ac6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 50412 characters]",
      "text/plain": [
       "<Figure size 800x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = plt.subplots(figsize=(8, 5))\n",
    "\n",
    "# Residui con barre d'errore\n",
    "ax.errorbar(\n",
    "    dfCalibro[\"F\"], dfCalibro[\"r\"],\n",
    "    xerr=dfCalibro[\"uF\"], yerr=dfCalibro[\"ur\"],\n",
    "    fmt='o', color=sns.color_palette()[0],\n",
    "    ecolor='gray', elinewidth=1, capsize=3,\n",
    "    markersize=5, label=\"Residui\"\n",
    ")\n",
    "\n",
    "# Linea dello zero\n",
    "ax.axhline(0, color='red', linestyle='--', linewidth=1)\n",
    "\n",
    "# Estetica\n",
    "sns.despine(ax=ax)\n",
    "ax.set_xlabel(\"F  [mN]\")\n",
    "ax.set_ylabel(r\"Residuo $x_i - \\left(\\frac{y_i}{A} - \\frac{B}{A}\\right)$  [mm]\")\n",
    "ax.set_title(\"Residui della regressione — Molla 1 (calibro)\")\n",
    "ax.legend()\n",
    "ax.grid(True, linestyle=':', linewidth=0.5, alpha=0.7)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5e02e3d",
   "metadata": {},
   "source": [
    "### Sezione non espressamente richiesta\n",
    "Dal grafico dei residui risulta chiaro che tutti i punti sono \"sotto\" mentre uno solo è \"sopra\".\n",
    "Per diletto personale ho voluto vedere cosa sarebbe successo escludendo quel punto.\n",
    "\n",
    "La sezione riguarda solo **Esclusione del punto 1**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 221,
   "id": "17cbeaea",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Fit completo:   chi²=55.10,  P=0.0000\n",
      "Senza punto 1:  chi²=1.794,  P=0.4078\n",
      "\n",
      "A:  23.9437 ± 0.0424  to  23.8835 ± 0.0432\n",
      "B:  2719.8777 ± 2.8546  to  2718.1378 ± 2.8613\n"
     ]
    }
   ],
   "source": [
    "mask = dfCalibro.index != 1\n",
    "dfCalibro_senza1 = dfCalibro.loc[mask]\n",
    "dfCalibro_senza1 = dfCalibro_senza1.copy()\n",
    "\n",
    "AC2, BC2, uAC2, uBC2, covABC2, chiC2, PC2 = reg_lin(-dfCalibro_senza1[\"dx\"], dfCalibro_senza1[\"F\"], dfCalibro_senza1[\"udx\"], dfCalibro_senza1[\"uF\"])\n",
    "\n",
    "print(f\"Fit completo:   chi²={chiC:.2f},  P={PC:.4f}\")\n",
    "print(f\"Senza punto 1:  chi²={chiC2:.3f},  P={PC2:.4f}\")\n",
    "print()\n",
    "print(f\"A:  {AC:.4f} ± {uAC:.4f}  to  {AC2:.4f} ± {uAC2:.4f}\")\n",
    "print(f\"B:  {BC:.4f} ± {uBC:.4f}  to  {BC2:.4f} ± {uBC2:.4f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 222,
   "id": "0bc01605",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "2cd706cd740242dc8c4f0d77578ff0cf",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Sheet(cells=(Cell(column_end=0, column_start=0, row_end=4, row_start=0, squeeze_row=False, type='numeric', val…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "05f2a07809a448b9a2b8c385f180ec43",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Sheet(cells=(Cell(column_end=0, column_start=0, row_start=0, squeeze_row=False, type='numeric', value=[88.97, …"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Ricalcolo Residui\n",
    "dfCalibro_senza1[\"r\"] = r_fn(\n",
    "  -dfCalibro_senza1[\"dx\"],\n",
    "   dfCalibro_senza1[\"F\"],\n",
    "   AC2, BC2\n",
    ")\n",
    "\n",
    "dfCalibro_senza1[\"ur\"] = sigma_r_fn(\n",
    "  -dfCalibro_senza1[\"dx\"], dfCalibro_senza1[\"F\"],\n",
    "   AC2, BC2,\n",
    "   dfCalibro_senza1[\"udx\"], dfCalibro_senza1[\"uF\"],\n",
    "   uAC2, uBC2, covABC2\n",
    ")\n",
    "\n",
    "sheet = ipysheet.from_dataframe(dfCalibro)\n",
    "display(sheet)\n",
    "sheet = ipysheet.from_dataframe(dfCalibro_senza1)\n",
    "display(sheet)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 223,
   "id": "d2c1a550",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 66216 characters]",
      "text/plain": [
       "<Figure size 1300x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Grafico ringraziate Cloude per l'idea super compatta di questo codice\n",
    "# WARNING: non intuitivo, ma molto molto figo\n",
    "fig, axes = plt.subplots(1, 2, figsize=(13, 5), sharey=True)\n",
    "\n",
    "for ax, (r_col, ur_col, f_col, uf_col, df_plot, titolo) in zip(axes, [\n",
    "  (\"r\",  \"ur\",  \"F\", \"uF\", dfCalibro, \"Fit completo (tutti i punti)\"),\n",
    "  (\"r\", \"ur\", \"F\", \"uF\",   dfCalibro_senza1,  \"Fit senza punto 1\"),\n",
    "]):\n",
    "  ax.errorbar(\n",
    "    df_plot[f_col], df_plot[r_col],\n",
    "    xerr=df_plot[uf_col], yerr=df_plot[ur_col],\n",
    "    fmt='o', color=sns.color_palette()[0],\n",
    "    ecolor='gray', elinewidth=1, capsize=3,\n",
    "    markersize=5, label=\"Residui\"\n",
    "  )\n",
    "  ax.axhline(0, color='red', linestyle='--', linewidth=1)\n",
    "  sns.despine(ax=ax)\n",
    "  ax.set_xlabel(\"F  [mN]\")\n",
    "  ax.set_title(titolo)\n",
    "  ax.grid(True, linestyle=':', linewidth=0.5, alpha=0.7)\n",
    "\n",
    "axes[0].set_ylabel(r\"Residuo $x_i - \\left(\\frac{y_i}{A} - \\frac{B}{A}\\right)$  [mm]\")\n",
    "fig.suptitle(\"Confronto residui — Molla 1 (calibro)\", y=1.02)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "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.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
