{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "1bba0128",
   "metadata": {},
   "source": [
    "# Simple fits\n",
    "\n",
    "Here we are fitting a line from scratch.\n",
    "In the next notebook, we will do fancier fits with neural networks, but let's start with a basic problem and complicate it as we go along.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "23feddde",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Tuple\n",
    "\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb1286f0",
   "metadata": {},
   "source": [
    "We start by generating some fake dataset, which is simple enough that we can visualize the results easily. For this reason, the dataset will contain only two  variables.\n",
    "\n",
    "The simulated example data will be $f(x) = 3 x + \\epsilon$, where $\\epsilon \\sim \\mathcal{N}(\\mu=0, \\sigma=0.5)$.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "5d457cd8",
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_data(N: int) -> np.ndarray:\n",
    "    x = 2*np.random.randn(N, 1)\n",
    "    epsilon = 0.5*np.random.randn(N, 1)\n",
    "    z = 3*x + epsilon\n",
    "    return np.concatenate((x, z), axis=1).astype(np.float32)\n",
    "\n",
    "data = generate_data(N=1000)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "bc7938ee-c442-428a-984b-47975605b850",
   "metadata": {},
   "outputs": [],
   "source": [
    "data = np.concatenate([data,\n",
    "                       np.array([[10.0, -4.0],\n",
    "                                 [5.0, -5.0]])\n",
    "                      ], axis=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48433f6f",
   "metadata": {},
   "source": [
    "We can fit this line from scratch, assuming $y = f(x) = \\beta x + \\alpha + \\epsilon$, where $\\epsilon$ is a zero-mean Gaussian noise."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a170344-34ff-4af6-9a6c-daa44b95408e",
   "metadata": {},
   "source": [
    "We can do this in two ways. In both cases, we start assuming the knowledge of $f(x)$ and use the Bayes Theorem:\n",
    "\n",
    "$ p(\\alpha, \\beta|\\mathcal{D}) \\, \\propto p(\\mathcal{D}|\\alpha, \\beta) \\, p(\\alpha) \\, p(\\beta)$\n",
    "\n",
    "We assume $p(\\alpha)$ and $p(\\beta)$ are constants, so we just need to maximize the likelihood $p(\\mathcal{D}|\\alpha, \\beta)$.\n",
    "That likelihood is a Gaussian distribution, because we assumed $y$ is exactly equals to a line plus some Gaussian noise coming from $\\epsilon$.\n",
    "\n",
    "$p(\\alpha,\\beta|\\mathcal{D}) \\propto \\prod \\mathcal{N}(y_i|\\text{mean}=f(x_i), \\text{std. dev.}=\\sigma_\\epsilon)$\n",
    "\n",
    "This leads to minimizing $L = \\sum_i (y_i - \\beta x_i - \\alpha)^2$, as seen in the lecture (simply take $- \\log p$ and to maximize $p$, one has to minimize $-\\log p$).\n",
    "\n",
    "We can try finding out $\\beta$ and $\\alpha$ by taking the derivative of $L$ and setting it to zero:\n",
    "\n",
    "$\\frac{d L}{d\\beta} = 2 \\sum_i (y_i - \\beta x_i - \\alpha) x_i = 0$\n",
    "\n",
    "$\\frac{d L}{d\\alpha} = -2 \\sum_i (y_i - \\beta x_i - \\alpha) = 0$\n",
    "\n",
    "Re-arranging:\n",
    "\n",
    "$2 \\sum_i (y_i x_i - \\beta x_i^2 - \\alpha x_i) = 0$\n",
    "\n",
    "$2 \\sum_i (y_i - \\beta x_i - \\alpha) = 0$\n",
    "\n",
    "This leads to:\n",
    "\n",
    "$ \\beta \\sum_i x_i^2 = \\sum_i (y_i - \\alpha) x_i$\n",
    "\n",
    "$ \\alpha = 1/N \\sum_i y_i - \\beta x_i = \\mathbb{E}[y - \\beta x]$\n",
    "\n",
    "We can then substitute $\\alpha$ in $\\beta$:\n",
    "\n",
    "$ \\beta \\sum_i x_i^2 = \\sum_i y_i x_i - 1/N \\sum_i \\sum_j (y_j - \\beta x_j) x_i$\n",
    "\n",
    "And expand it:\n",
    "\n",
    "$ \\beta (\\sum_i x_i^2 - 1/N \\sum_i \\sum_j x_i x_j) = \\sum_i y_i x_i - 1/N \\sum_i \\sum_j y_j x_i$\n",
    "\n",
    "Taking $x_j$ from a sum that does not depend on $j$, we see that the second term is just the square of the sum over $x_i$:\n",
    "\n",
    "$ \\beta (\\sum_i x_i^2 - 1/N(\\sum_{i} x_i)^2 ) = \\sum_i y_i x_i - 1/N \\sum_i \\sum_j y_j x_i$\n",
    "\n",
    "Dividing by the number of points $N$, several sums reduce to averages, which make this much easier to interpret if we use some standard identities from statistics.\n",
    "For example $1/N \\sum_i x_i^2 - 1/N^2 (\\sum_i x_i)^2$ is a known expression for the variance of $X$. \n",
    "\n",
    "$ \\beta \\text{var}[X] = \\mathbb{E}[XY] - \\mathbb{E}[X]\\mathbb{E}[Y]$\n",
    "\n",
    "And the right-hand-side is a known expression for the covariance between $X$ and $Y$:\n",
    "\n",
    "$ \\beta \\text{var}[X] = \\text{cov}[X,Y]$\n",
    "\n",
    "This leads to:\n",
    "\n",
    "$\\beta = \\frac{\\text{cov}[X,Y]}{\\text{var}[X]}$\n",
    "\n",
    "This, together with the expression from above for $\\alpha$, allows us to easily fit a line in a few lines of code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "30205402",
   "metadata": {},
   "outputs": [],
   "source": [
    "cov = np.cov(data[:,0], data[:,1])[0,1]\n",
    "varX = np.std(data[:,0])**2\n",
    "beta = cov/varX\n",
    "alpha = np.mean(data[:,1] - beta*data[:,0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "cabdfc12-3033-4a2f-96ed-4ab1ea8312ab",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.legend.Legend at 0x7fdf8b28ccd0>"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 32760 characters]",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.scatter(data[:,0], data[:,1], label=\"Data\")\n",
    "plt.axline((0, alpha), slope=beta, c='r', lw=2, ls='--', label=\"Line fit\")\n",
    "plt.xlabel(r\"$x$\")\n",
    "plt.ylabel(r\"$y$\")\n",
    "plt.legend(frameon=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd74e2d4-b75b-4235-8541-d5565ec4d7a8",
   "metadata": {},
   "source": [
    "Try comparing the fit result (values of $\\alpha$ and $\\beta$) with the simulated values in the beginning of the notebook. Are they close? Which criteria should you use to establish that they are close?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "c6a1998f-81eb-4a32-8d00-04639ca777c2",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "-0.06067522680624462"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "alpha"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "ec33c149-7d6f-433b-872a-ca97c0b7d7d6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2.89487707822029"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "beta"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "91f0020f-6be7-4d3f-898c-2b062f825959",
   "metadata": {},
   "source": [
    "# Why is this a terrible idea?\n",
    "\n",
    "The previous calculation assumes that $p(\\alpha)$ and $p(\\beta)$, that is, the priors, are uniform. That means that any value of $\\alpha$ and $\\beta$ is acceptable. As we have discussed in the previous lecture, this leads to a fit that is very vulnerable to outliers. In the next notebook we will try to fix this by changing the priors to give a higher weight to values of $\\alpha$ and $\\beta$ close to zero, by setting $p(\\alpha)$ and $p(\\beta)$ to a Gaussian distribution, making an assumption on $\\alpha$ and $\\beta$ before looking at data. This method is called *regularization* and it starts from the assumption that, if the data has been previously *standardized*, we tend not to get wildly large values of a single parameter.\n",
    "\n",
    "Nevertheless, the calculation above can be changed to consider a Gaussian $p(\\alpha)$ and $p(\\beta)$ without using the methods described in the next notebook. Feel free to try redoing the calculation at home considering that.\n",
    "\n",
    "There are many other regularization techniques that make different assumptions on the parameters before looking at data. The choice of this method is strongly dependent on the problem at hand and it applies not only to line fitting, but fits in general.\n",
    "\n",
    "Try for example to add a data point in the variable `data` that is very far off to simulate an outlier. What happens with the fit? Are the fit parameters still close?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6cc1b58d-08fc-4f83-8aa4-e2979aa92c89",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "2bdcc1c0-427c-4f6c-83d3-2f39a498cfa1",
   "metadata": {},
   "source": [
    "Another problem with the procedure above is that it does not calculate the errors on $\\alpha$ and $\\beta$. We will also cover this in the next notebook.\n",
    "\n",
    "One way of estimating uncertainties from the previous calculations is by making the approximation that the posterior probability $p(\\alpha, \\beta|\\mathcal{D})$ is approximately Gaussian around the best values of $\\alpha$ and $\\beta$ (this is called the *Laplace approximation*). In the example above, this is exactly true, but depending on the likelihood and priors this is not always so, although the approximation tends to be relatively good in several common cases. If we assume the posterior is Gaussian, the variance of the posterior can be taken as a measure of the uncertainty squared of $\\alpha$ and $\\beta$. How could we calculate that variance?\n",
    "\n",
    "One theorem called the Cramer Rao Lower Bound shows that the covariance matrix of the \n",
    "It can be shown, furthermore, that the covariance matrix of the posterior, $C$ satisfies:\n",
    "\n",
    "$C - I^{-1}$ is semi-positive definite,\n",
    "\n",
    "where $I_{ij} = -\\mathbb{E}[\\frac{\\partial^2 L}{\\partial \\theta_i \\partial \\theta_j}]$ (this is called the *Fisher information matrix*) and $L$ is the negative log-posterior, as above.\n",
    "\n",
    "This means, within the context of the calculation above, that the variance of the posterior could be said to have a *lower bound* of at least $-(\\frac{\\partial^2 L}{\\partial^2 \\theta})^{-1}$, calculated at the optimal values of the parameters.\n",
    "\n",
    "By taking this second derivative we could get estimates to the variances of the parameters. This is often how curve-fitting algorithms establish approximate uncertainties for the parameters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0812975c-a8fc-4d9e-98bc-d3851e75be70",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.10.12"
  },
  "widgets": {
   "application/vnd.jupyter.widget-state+json": {
    "state": {},
    "version_major": 2,
    "version_minor": 0
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
