Last active
March 21, 2022 10:55
-
-
Save tspspi/b07211a88097234ec9fb09be4a6f9577 to your computer and use it in GitHub Desktop.
Basics-Least squares using QR decomposition
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "markdown", | |
"id": "cdef5c8e", | |
"metadata": {}, | |
"source": [ | |
"## Introduction\n", | |
"\n", | |
"In this notebook there will be a short implementation of the least squares minimization approach using QR decomposition as has been described in the blog article at https://www.tspi.at/2021/12/18/leastsquaresqr.html. Basically the idea is to solve an overdetermined system of linear equations with the best possible solution - a solution that minimizes the residuum (or error).\n", | |
"\n", | |
"The set of linear equations that should be satisfied is described by $A \\vec{x} = \\vec{b}$. The matrix $A$ contains the coefficients for the variables $\\vec{x}$, the constants are all adsorbed in a constant vector $\\vec{b}$. The basic idea is to minimize the residuum $\\mid A \\vec{x} - \\vec{b} \\mid^2_2 \\to \\text{min}$. This will be done by applying QR decomposition to the coefficient matrix $A$. Details can be found in the blog post linked above.\n", | |
"\n", | |
"The basic idea is that we want to solve\n", | |
"\n", | |
"$$\n", | |
"A \\vec{x} = \\vec{b}\n", | |
"$$\n", | |
"\n", | |
"In the optimal case this would imply\n", | |
"\n", | |
"$$\n", | |
"A \\vec{x} - \\vec{b} = 0\n", | |
"$$\n", | |
"\n", | |
"Since we only search for the best approximation (also in case there is no exact solution) we want to minimize the residuum:\n", | |
"\n", | |
"$$\n", | |
"\\mid A \\vec{x} - \\vec{b} \\mid^2_2 \\to \\text{min}\n", | |
"$$\n", | |
"\n", | |
"Now one can apply QR decomposition to $A$:\n", | |
"\n", | |
"$$\n", | |
"\\mid QR \\vec{x} - \\vec{b} \\mid^2_2 \\to \\text{min}\n", | |
"$$\n", | |
"\n", | |
"Since Q is an orthogonal matrix - this means it doesn't change the length of the vector but only it's orientation - and we're only interested in minimizing it's length one can simply apply the orthogonal transformation $Q^T$:\n", | |
"\n", | |
"$$\n", | |
"\\mid Q^T (QR \\vec{x} - \\vec{b}) \\mid^2_2 \\to \\text{min} \\\\ \n", | |
"\\mid \\underbrace{Q^T Q}_{1} R \\vec{x} - Q^T \\vec{b} \\mid^2_2 \\to \\text{min} \\\\ \n", | |
"\\mid R \\vec{x} - Q^T \\vec{b} \\mid^2_2 \\to \\text{min} \\\\ \n", | |
"\\to R \\vec{x} - Q^T \\vec{b} = 0 \\\\\n", | |
"\\to R \\vec{x} = Q^T \\vec{b}\n", | |
"$$\n", | |
"\n", | |
"As one can see this reduces the problem of least squares to a simple set of linear equations with an upper triangular matrix $R$ and a simple to calculate matrix $Q^T$. The matrix vector product $Q^T \\vec{b}$ is easy to calculate.\n", | |
"\n", | |
"The matrix $R$ might contain rows that are fully set to zero. These do not contribute to the solution of the problem but can be used to determine the quality of the fit. The structure allows one to simply perform backsubstitution to solve the remaining equations:\n", | |
"\n", | |
"$$\n", | |
"\\vec{c} = Q^T \\vec{b} \\\\ \n", | |
"x_{\\alpha} = \\frac{c_{\\alpha} - \\sum_{i = \\alpha+1}^{n} x_i * r_{\\alpha,i}}{r_{\\alpha,\\alpha}}\n", | |
"$$" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"id": "583110e0", | |
"metadata": {}, | |
"source": [ | |
"### Simple matrix class\n", | |
"\n", | |
"Again a simple matrix class will be used. In addition the [QR decomposition function using Givens rotations](/2021/12/08/qrgivens.html) will be included to allow simple QR decomposition though in many cases an implementation based on Householder reflections might be more appropriate." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 1, | |
"id": "84186f6f", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"import math" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 2, | |
"id": "3248bc96", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class Matrix:\n", | |
" def __init__(self, rows, cols, *, rounding = 8, initList = None, name = None, description = None):\n", | |
" self.name = name\n", | |
" self.description = description\n", | |
" self.rows = rows\n", | |
" self.cols = cols\n", | |
" self.data = [ 0 ] * rows\n", | |
" self.rounding = rounding\n", | |
" for row in range(rows):\n", | |
" self.data[row] = [ 0.0 ] * cols\n", | |
" # The following would NOT work since we create the same copy of the\n", | |
" # lists and reference them\n", | |
" # self.data = [ [ 0.0 ] * cols ] * rows\n", | |
" if initList:\n", | |
" if not isinstance(initList, list):\n", | |
" raise ValueError(\"List initializer has to be a list ...\")\n", | |
" # Two ways: Either one list with rows * cols entries just ordered in row major\n", | |
" # order (first row, second row, etc.) or nested list\n", | |
" if len(initList) == (rows * cols):\n", | |
" for row in range(rows):\n", | |
" for col in range(cols):\n", | |
" try:\n", | |
" k = float(initList[col + row * cols])\n", | |
" self.data[row][col] = round(k, self.rounding)\n", | |
" except ValueError:\n", | |
" raise ValueError(\"Initializer list has to contain numbers\")\n", | |
" else:\n", | |
" if len(initList) != rows:\n", | |
" raise ValueError(\"Row mismatch in initializer list\")\n", | |
" for row in range(rows):\n", | |
" if not isinstance(initList[row], list):\n", | |
" raise ValueError(\"Columns have to be lists in initializer\")\n", | |
" if len(initList[row]) != cols:\n", | |
" raise ValueError(\"Column mismatch in initializer list\")\n", | |
" for col in range(cols):\n", | |
" try:\n", | |
" k = float(initList[row][col])\n", | |
" self.data[row][col] = round(k, self.rounding)\n", | |
" except ValueError:\n", | |
" raise ValueError(\"Initializer list has to contain numbers\")\n", | |
" def get(self, row, col):\n", | |
" if (row < 0) or (row >= self.rows):\n", | |
" raise IndexError(\"Row {} invalid (matrix is {} x {})\".format(row, self.rows, self.cols))\n", | |
" if (col < 0) or (col >= self.cols):\n", | |
" raise IndexError(\"Column {} invalid (matrix is {} x {})\".format(col, self.rows, self.cols))\n", | |
" return self.data[row][col]\n", | |
" def set(self, row, col, value):\n", | |
" if (row < 0) or (row >= self.rows):\n", | |
" raise IndexError(\"Row {} invalid (matrix is {} x {})\".format(row, self.rows, self.cols))\n", | |
" if (col < 0) or (col >= self.cols):\n", | |
" raise IndexError(\"Column {} invalid (matrix is {} x {})\".format(col, self.rows, self.cols))\n", | |
" self.data[row][col] = round(value, self.rounding)\n", | |
" def copy(self, *, name = None, description = None):\n", | |
" result = Matrix(self.rows, self.cols, name = name, description = description)\n", | |
" for row in range(self.rows):\n", | |
" for col in range(self.cols):\n", | |
" result.set(row, col, self.get(row, col))\n", | |
" return result\n", | |
" def getName(self):\n", | |
" return self.name\n", | |
" def getDescription(self):\n", | |
" return self.description\n", | |
" def __str__(self):\n", | |
" resString = \"\"\n", | |
" if self.name:\n", | |
" resString = resString + \"{} = \".format(self.name)\n", | |
" for row in range(self.rows):\n", | |
" for col in range(self.cols):\n", | |
" resString = resString + \"{}\".format(self.data[row][col]) + \"\\t\"\n", | |
" resString = resString + \"\\n\"\n", | |
" return resString\n", | |
" def transpose(self, *, name = None, description = None):\n", | |
" result = Matrix(self.cols, self.rows, name = name, description = description)\n", | |
" for col in range(self.cols):\n", | |
" for row in range(self.rows):\n", | |
" result.set(col, row, self.get(row, col))\n", | |
" return result\n", | |
" def add(self, other, *, name = None, description = None):\n", | |
" if not isinstance(other, Matrix):\n", | |
" raise ValueError(\"Adding matrix with non matrix is not supported\")\n", | |
" if (other.cols != self.cols) or (other.rows != self.rows):\n", | |
" raise ValueError(\"Addition requires same dimensions\")\n", | |
" result = Matrix(self.rows, self.cols, name = name, description = description)\n", | |
" for col in range(self.cols):\n", | |
" for row in range(self.rows):\n", | |
" result.set(row, col, round(self.get(row, col) + other.get(row, col), self.rounding))\n", | |
" return result\n", | |
" def sub(self, other, *, name = None, description = None):\n", | |
" if not isinstance(other, Matrix):\n", | |
" raise ValueError(\"Adding matrix with non matrix is not supported\")\n", | |
" if (other.cols != self.cols) or (other.rows != self.rows):\n", | |
" raise ValueError(\"Addition requires same dimensions\")\n", | |
" result = Matrix(self.rows, self.cols, name = name, description = description)\n", | |
" for col in range(self.cols):\n", | |
" for row in range(self.rows):\n", | |
" result.set(row, col, round(self.get(row, col) - other.get(row, col), self.rounding))\n", | |
" return result\n", | |
" def multiply(self, other, *, name = None, description = None):\n", | |
" # Calculate self * other\n", | |
" if isinstance(other, Matrix):\n", | |
" if not self.cols == other.rows:\n", | |
" raise ValueError(\"Colums of left side {} have to match rows of right side {}\".format(self.cols, other.rows))\n", | |
" result = Matrix(self.rows, other.cols, name = name, description = description)\n", | |
" for row in range(self.rows):\n", | |
" for col in range(other.cols):\n", | |
" t = 0\n", | |
" for k in range(self.cols):\n", | |
" t = t + self.get(row, k) * other.get(k, col)\n", | |
" t = round(t, 4)\n", | |
" result.set(row, col, round(t, self.rounding))\n", | |
" return result\n", | |
" else:\n", | |
" try:\n", | |
" otherFloat = float(other)\n", | |
" result = Matrix(self.rows, self.cols, name = name, description = description)\n", | |
" for r in range(self.rows):\n", | |
" for c in range(self.cols):\n", | |
" result.set(r,c, round(self.get(r,c,)*otherFloat, self.rounding))\n", | |
" return result\n", | |
" except ValueError:\n", | |
" raise ValueError(\"Matrix has to be multiplied by matrix or scalar\")" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 3, | |
"id": "76ffd930", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class IdentityMatrix(Matrix):\n", | |
" def __init__(self, size, *, name = None, description = None):\n", | |
" super().__init__(size, size, name = name, description = description)\n", | |
" for k in range(size):\n", | |
" self.set(k,k,1)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 4, | |
"id": "571a598d", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class GivensRotationMatrix(IdentityMatrix):\n", | |
" def __init__(self, size, i, j, x1, x2, *, name = None, description = None):\n", | |
" super().__init__(size, name = name, description = description)\n", | |
" if (i >= size) or (i < 0) or (j >= size) or (j < 0):\n", | |
" raise ValueError(\"Invalid first index i or j\")\n", | |
" # Calculate s and c\n", | |
" s = 1.0 / math.sqrt(x1 * x1 / (x2 * x2) + 1)\n", | |
" c = s * x1 / x2\n", | |
" self.set(i,i,round(c, self.rounding))\n", | |
" self.set(j,i,round(-1 * s, self.rounding))\n", | |
" self.set(i,j,round(s, self.rounding))\n", | |
" self.set(j,j,round(c, self.rounding))" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 5, | |
"id": "bfeb5286", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"def QRDecomposeWithGivens(matIn):\n", | |
" matR = matIn.copy()\n", | |
" matQ = IdentityMatrix(matIn.rows)\n", | |
" for col in range(matIn.cols):\n", | |
" for row in range(matIn.rows-1, col, -1):\n", | |
" if matR.get(row,col) == 0:\n", | |
" continue\n", | |
" rotationMatrix = GivensRotationMatrix(matIn.rows, row-1, row, matR.get(row-1, col), matR.get(row,col))\n", | |
" matR = rotationMatrix.multiply(matR)\n", | |
" matQ = matQ.multiply(rotationMatrix.transpose())\n", | |
"\n", | |
" return (matQ, matR)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"id": "2f634341", | |
"metadata": {}, | |
"source": [ | |
"## Implementing least squares" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"id": "81798434", | |
"metadata": {}, | |
"source": [ | |
"The whole procedure will be wrapped into a single ```leastSquares``` function that just accepts the coefficient matrix and the constant vector. In the end it will return a variable vector $\\vec{x}$. Note that this naive implementation assumes that the system is not underdetermined." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 6, | |
"id": "3500b858", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"def leastSquares_CheckRowEmpty(matA, row, *, epsilon = 1e-3):\n", | |
" for k in range(matA.cols):\n", | |
" v = matA.get(row, k)\n", | |
" if v < (-1.0 * epsilon) or v > epsilon:\n", | |
" return False\n", | |
" return True\n", | |
"\n", | |
"\n", | |
"def leastSquares(matA, vecB):\n", | |
" if not isinstance(matA, Matrix):\n", | |
" raise ValueError(\"Coefficient matrix has to be a matrix type\")\n", | |
" if not isinstance(vecB, Matrix):\n", | |
" raise ValueError(\"Constant matrix has to be a vector (matrix) type\")\n", | |
" if vecB.cols != 1:\n", | |
" raise ValueError(\"Constant vector has to have only one row\")\n", | |
" if vecB.rows != matA.rows:\n", | |
" raise ValueError(\"Columns in coefficient matrix have to match the number of constants in constant vector\")\n", | |
"\n", | |
" q,r = QRDecomposeWithGivens(matA)\n", | |
" bTransformed = q.transpose().multiply(vecB)\n", | |
"\n", | |
" # Check how many rows we have to drop (i.e. how many variables we're going to ignore)\n", | |
" populatedRows = matA.rows\n", | |
" while leastSquares_CheckRowEmpty(r, populatedRows - 1) and (populatedRows > 0):\n", | |
" populatedRows = populatedRows - 1\n", | |
" if populatedRows == 0:\n", | |
" return None\n", | |
"\n", | |
" # Initialize our result vector that will contain our best fit for our unknowns\n", | |
" xResult = [ 0.0 ] * matA.cols\n", | |
"\n", | |
" # Now perform backsubstitution ...\n", | |
" for row in range(populatedRows-1, -1, -1):\n", | |
" t = bTransformed.get(row, 0)\n", | |
" for i in range(row+1, populatedRows):\n", | |
" t = t - xResult[i] * r.get(row, i)\n", | |
" t = t / r.get(row, row)\n", | |
" xResult[row] = t\n", | |
"\n", | |
" return xResult" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"id": "e60c59eb", | |
"metadata": {}, | |
"source": [ | |
"### Two mini examples\n", | |
"\n", | |
"In the following section I've ran my implementation against two sets of equations that I knew the solutions for by having them calculated by hand and having them compared to solutions someone else had calculated. This is of course no formal proof that the implementation works but it's more likely it's correct in this case." | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 7, | |
"id": "0955a832", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"A:\n", | |
"2.0\t0.0\t\n", | |
"0.0\t1.0\t\n", | |
"1.0\t2.0\t\n", | |
"\n", | |
"b:\n", | |
"1.0\t\n", | |
"0.0\t\n", | |
"3.0\t\n", | |
"\n", | |
"approxX:\n", | |
"[0.6190413105929794, 0.9523275104908754]\n", | |
"\n", | |
"\n", | |
"A:\n", | |
"2.0\t3.0\t\n", | |
"-3.0\t-4.0\t\n", | |
"\n", | |
"b:\n", | |
"-6.0\t\n", | |
"7.0\t\n", | |
"\n", | |
"approxX:\n", | |
"[3.000360460272565, -4.000360490266763]\n" | |
] | |
} | |
], | |
"source": [ | |
"# Example 1\n", | |
"matA = Matrix(3, 2, initList = [ [ 2, 0 ], [ 0, 1 ], [ 1, 2 ] ] )\n", | |
"matB = Matrix(3, 1, initList = [ 1, 0, 3 ] )\n", | |
"print(\"A:\")\n", | |
"print(matA)\n", | |
"print(\"b:\")\n", | |
"print(matB)\n", | |
"print(\"approxX:\")\n", | |
"print(leastSquares(matA, matB))\n", | |
"\n", | |
"# Example 2\n", | |
"matA = Matrix(2, 2, initList = [ [ 2, 3 ], [ -3, -4 ] ] )\n", | |
"matB = Matrix(2, 1, initList = [ -6, 7 ] )\n", | |
"print(\"\\n\\nA:\")\n", | |
"print(matA)\n", | |
"print(\"b:\")\n", | |
"print(matB)\n", | |
"print(\"approxX:\")\n", | |
"print(leastSquares(matA, matB))" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"id": "46bfa3c7", | |
"metadata": {}, | |
"source": [ | |
"## Using least squares to fit linear functions\n", | |
"\n", | |
"This is a pretty simple application of least squares. The idea is to have a set of data points $(x,y)$ that should in optimal case fulfill a linear equation $f(x) = k * x + d$. Usually measured data of course doesn't exactly fit one's model due to noise or modelling errors. So one can use least squares to perform linear regression - one can try to find the best approximation for a given dataset with ones model function.\n", | |
"\n", | |
"In this case the coefficient matrix $A$ is formed by the prefactors of $k$ and the constant $1$ for the unknowns $k$ and $d$ in the equation $k*x + d*1$. The constants are the $y$ values.\n", | |
"\n", | |
"In the following example a measured dataset was given by\n", | |
"\n", | |
"| y | x |\n", | |
"| --- | --- |\n", | |
"| 4 | 2 |\n", | |
"| 5 | 3 |\n", | |
"| 7 | 5 |\n", | |
"| 10 | 7 |\n", | |
"| 15 | 9 |\n", | |
"\n", | |
"Least squares will be used to find the best solution for $k$ and $d$ for the following equations:\n", | |
"\n", | |
"$$\n", | |
" 4 = 2 * k + d \\\\\n", | |
" 5 = 3 * k + d \\\\\n", | |
" 7 = 5 * k + d \\\\\n", | |
"10 = 7 * k + d \\\\\n", | |
"15 = 9 * k + d\n", | |
"$$\n", | |
"\n", | |
"The coefficient matrix is given by\n", | |
"\n", | |
"$$\n", | |
"A = \\begin{pmatrix}\n", | |
"2 & 1 \\\\ \n", | |
"3 & 1 \\\\ \n", | |
"5 & 1 \\\\ \n", | |
"7 & 1 \\\\ \n", | |
"9 & 1\n", | |
"\\end{pmatrix}\n", | |
"$$\n", | |
"\n", | |
"The constant vector is formed by\n", | |
"\n", | |
"$$\n", | |
"b = \\begin{pmatrix} 4 \\\\ 5 \\\\ 7 \\\\ 10 \\\\ 15 \\end{pmatrix}\n", | |
"$$" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 8, | |
"id": "088289d2", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"\n", | |
"\n", | |
"A:\n", | |
"2.0\t1.0\t\n", | |
"3.0\t1.0\t\n", | |
"5.0\t1.0\t\n", | |
"7.0\t1.0\t\n", | |
"9.0\t1.0\t\n", | |
"\n", | |
"b:\n", | |
"4.0\t\n", | |
"5.0\t\n", | |
"7.0\t\n", | |
"10.0\t\n", | |
"15.0\t\n", | |
"\n", | |
"approxX:\n", | |
"[1.517810419839562, 0.3084008097165992]\n" | |
] | |
} | |
], | |
"source": [ | |
"matA = Matrix(5, 2, initList = [ [ 2, 1 ], [ 3, 1 ], [ 5, 1 ], [ 7, 1 ], [ 9, 1 ] ] )\n", | |
"matB = Matrix(5, 1, initList = [ 4, 5, 7, 10, 15 ] )\n", | |
"print(\"\\n\\nA:\")\n", | |
"print(matA)\n", | |
"print(\"b:\")\n", | |
"print(matB)\n", | |
"print(\"approxX:\")\n", | |
"fitResult = leastSquares(matA, matB)\n", | |
"print(fitResult)" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"id": "73356a70", | |
"metadata": {}, | |
"source": [ | |
"Now lets visualize the result" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 9, | |
"id": "947fba63", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"import matplotlib.pyplot as plt" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 10, | |
"id": "badb68d4", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"image/png": "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\n", | |
"text/plain": [ | |
"<Figure size 432x288 with 1 Axes>" | |
] | |
}, | |
"metadata": { | |
"needs_background": "light" | |
}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"# First plot our fitted model\n", | |
"x = range(0,12,1)\n", | |
"y = [ 0.0 ] * len(x)\n", | |
"for i in range(len(x)):\n", | |
" y[i] = x[i] * fitResult[0] + fitResult[1]\n", | |
"\n", | |
"# Then plot our data\n", | |
"x2 = [ 2, 3, 5, 7, 9 ]\n", | |
"y2 = [ 4, 5, 7, 10, 15 ]\n", | |
"\n", | |
"plt.plot(x,y)\n", | |
"plt.plot(x2, y2, linestyle=\"\", marker=\"o\")\n", | |
"plt.title(\"Linear regression\")\n", | |
"plt.grid()" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"id": "142e83ae", | |
"metadata": {}, | |
"source": [ | |
"## Simple quadratic regression\n", | |
"\n", | |
"One can of course use the linear least squares method to fit arbitrary polynomial functions. One can for example also apply this to quadratic functions. Let's assume our model function is\n", | |
"\n", | |
"$$\n", | |
"y = ax^2 + bx + c\n", | |
"$$\n", | |
"\n", | |
"And we supply a set of points that might reflect our measured values:\n", | |
"\n", | |
"$$\n", | |
"(0,1) \\\\\n", | |
"(1,3) \\\\ \n", | |
"(4,7) \\\\ \n", | |
"(5,8) \\\\ \n", | |
"(7,20) \\\\ \n", | |
"(10,25)\n", | |
"$$\n", | |
"\n", | |
"This would lead to a (not exactly solvable) set of linear equations by simply substituting the points into the equations:\n", | |
"\n", | |
"$$\n", | |
"1 = a*0 + b*0 + c \\\\ \n", | |
"3 = a*1^2 + b*1 + c \\\\ \n", | |
"7 = a*4^2 + b*4 + c \\\\ \n", | |
"8 = a*5^2 + b*5 + c \\\\ \n", | |
"20 = a*7^2 + b*7 + c \\\\ \n", | |
"25 = a*10^2 + b*10 + c\n", | |
"$$\n", | |
"\n", | |
"As one can see this is a simple set of linear equations as before:\n", | |
"\n", | |
"$$\n", | |
"1 = c \\\\ \n", | |
"3 = a*1 + b*1 + c \\\\ \n", | |
"7 = a*16 + b*4 + c \\\\ \n", | |
"8 = a*25 + b*5 + c \\\\ \n", | |
"20 = a*49 + b*7 + c \\\\ \n", | |
"25 = a*100 + b*10 + c\n", | |
"$$\n", | |
"\n", | |
"The following codeblock just builds the coefficient matrix $A$ by calculating the terms $x^2$, $x$ and the $1$ as well as the constant vector $\\vec{b}$ by inserting $y$ coordinates. The function accepts an arbitrary number of points:" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 11, | |
"id": "01875f1b", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"def quadraticRegression(points, trace = False, plot = False, plotRange = [-10, 10], plotSteps = 1):\n", | |
" matA2 = Matrix(len(points), 3)\n", | |
" matB2 = Matrix(len(points), 1)\n", | |
" for row in range(len(points)):\n", | |
" matA2.set(row, 0, points[row][0] * points[row][0])\n", | |
" matA2.set(row, 1, points[row][0])\n", | |
" matA2.set(row, 2, 1.0)\n", | |
" matB2.set(row, 0, points[row][1])\n", | |
"\n", | |
" if trace:\n", | |
" print(\"A:\")\n", | |
" print(matA2)\n", | |
" print(\"b:\")\n", | |
" print(matB2)\n", | |
"\n", | |
" fitResult = leastSquares(matA2, matB2)\n", | |
"\n", | |
" if trace:\n", | |
" print(\"Result:\")\n", | |
" print(fitResult)\n", | |
"\n", | |
" if plot:\n", | |
" x2 = [ 0.0 ] * len(points)\n", | |
" y2 = [ 0.0 ] * len(points)\n", | |
" for row in range(len(points)):\n", | |
" x2[row] = points[row][0]\n", | |
" y2[row] = points[row][1]\n", | |
"\n", | |
" x = range(plotRange[0], plotRange[1], plotSteps)\n", | |
" y = [ 0.0 ] * len(x)\n", | |
" for i in range(len(x)):\n", | |
" y[i] = x[i] * x[i] * fitResult[0] + x[i] * fitResult[1] + fitResult[2]\n", | |
"\n", | |
" plt.plot(x,y)\n", | |
" plt.plot(x2, y2, linestyle=\"\", marker=\"o\")\n", | |
" plt.title(\"Quadratic regression\")\n", | |
" plt.grid()\n", | |
"\n", | |
" return fitResult" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 12, | |
"id": "198fdede", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Parameters: [0.17054737581685814, 0.7278125943022667, 2.1137956163462075]\n" | |
] | |
}, | |
{ | |
"data": { | |
"image/png": "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\n", | |
"text/plain": [ | |
"<Figure size 432x288 with 1 Axes>" | |
] | |
}, | |
"metadata": { | |
"needs_background": "light" | |
}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"res = quadraticRegression(\n", | |
" [\n", | |
" [ -10, 10 ],\n", | |
" [ -8, 10 ],\n", | |
" [ 0, 1 ],\n", | |
" [ 1, 3 ],\n", | |
" [ 4, 7 ],\n", | |
" [ 5, 8 ],\n", | |
" [ 7, 20 ],\n", | |
" [ 10, 25 ]\n", | |
" ],\n", | |
" plot = True,\n", | |
" plotRange = [ - 15, 15 ]\n", | |
")\n", | |
"\n", | |
"print(\"Parameters: {}\".format(res))" | |
] | |
}, | |
{ | |
"cell_type": "markdown", | |
"id": "412741a4", | |
"metadata": {}, | |
"source": [ | |
"## Simple cubic fit\n", | |
"\n", | |
"As mentioned before this can be applied to any problem that reduces to a set of linear equations - for example also a cubic fitting problem - with minor modifications:\n", | |
"\n", | |
"$$\n", | |
"f(x) = a*x^3 + b*x^2 + c*x + d\n", | |
"$$" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 13, | |
"id": "2c2ea734", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"def cubicFit(points, trace = False, plot = False, plotRange = [-10, 10], plotSteps = 1):\n", | |
" matA2 = Matrix(len(points), 4)\n", | |
" matB2 = Matrix(len(points), 1)\n", | |
" for row in range(len(points)):\n", | |
" matA2.set(row, 0, points[row][0] * points[row][0] * points[row][0])\n", | |
" matA2.set(row, 1, points[row][0] * points[row][0])\n", | |
" matA2.set(row, 2, points[row][0])\n", | |
" matA2.set(row, 3, 1.0)\n", | |
" matB2.set(row, 0, points[row][1])\n", | |
"\n", | |
" if trace:\n", | |
" print(\"A:\")\n", | |
" print(matA2)\n", | |
" print(\"b:\")\n", | |
" print(matB2)\n", | |
"\n", | |
" fitResult = leastSquares(matA2, matB2)\n", | |
"\n", | |
" if trace:\n", | |
" print(\"Result:\")\n", | |
" print(fitResult)\n", | |
"\n", | |
" if plot:\n", | |
" x2 = [ 0.0 ] * len(points)\n", | |
" y2 = [ 0.0 ] * len(points)\n", | |
" for row in range(len(points)):\n", | |
" x2[row] = points[row][0]\n", | |
" y2[row] = points[row][1]\n", | |
"\n", | |
" x = range(plotRange[0], plotRange[1], plotSteps)\n", | |
" y = [ 0.0 ] * len(x)\n", | |
" for i in range(len(x)):\n", | |
" y[i] = x[i] * x[i] * x[i] * fitResult[0] + x[i] * x[i] * fitResult[1] + x[i] * fitResult[2] + fitResult[3]\n", | |
"\n", | |
" plt.plot(x,y)\n", | |
" plt.plot(x2, y2, linestyle=\"\", marker=\"o\")\n", | |
" plt.title(\"Cubic regression\")\n", | |
" plt.grid()\n", | |
"\n", | |
" return fitResult" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 14, | |
"id": "c66ae3c4", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"Parameters: [0.17054737581685814, 0.7278125943022667, 2.1137956163462075]\n" | |
] | |
}, | |
{ | |
"data": { | |
"image/png": "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\n", | |
"text/plain": [ | |
"<Figure size 432x288 with 1 Axes>" | |
] | |
}, | |
"metadata": { | |
"needs_background": "light" | |
}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"ret = cubicFit(\n", | |
" [\n", | |
" [ -23, -70 ],\n", | |
" [ -20, -30 ],\n", | |
" [ -15, -20 ],\n", | |
" [ -10, -10 ],\n", | |
" [ -8, -10 ],\n", | |
" [ 0, 1 ],\n", | |
" [ 1, 3 ],\n", | |
" [ 4, 7 ],\n", | |
" [ 5, 8 ],\n", | |
" [ 7, 20 ],\n", | |
" [ 10, 25 ],\n", | |
" [ 15, 100 ]\n", | |
" ],\n", | |
" trace = False,\n", | |
" plot = True,\n", | |
" plotRange = [ - 25, 25 ]\n", | |
")\n", | |
"\n", | |
"print(\"Parameters: {}\".format(res))" | |
] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 3", | |
"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.8.12" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 5 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment