This JavaScript program demonstrates how to generate and graph an ordinary least squares line. An ordinary least squares fit involves calculating a regression line for a set of random points. Given a set of points that are assumed to have a linear relationship, a line is fitted that minimizes the least squares error of the vertical distance to the line.
In ordinary least squares, data are collected at regular interval, typically it is time. These intervals are regular and assumed to be without error in the x-coordinate. The y-value is the value that we are concerned with approximating and it is assumed to be a linear function of x with a gaussian error.
Below, we generate a random line in black. Then Gaussian error terms are added to the y-values of that line to generate a theoretical set of points, in red, that we would expect to get from some measurements. The red line is the line that we would calculate as the best least squares fit. The vertical red lines show the error for each y-value relative to the least squares line.
<!DOCTYPE html> <html> <head> <title>XoaX.net's Javascript</title> <script type="text/javascript" src="OrdinaryLeastSquares.js"></script> </head> <body onload="Initialize()"> <div style="width:848px;height:630px;"> <div style="width:48px;height:630px;float:left;"> <input id="idHighY" type="text" size="3" style="width:40px;height:16px;"/> <div style="width:48px;height:556px;"></div> <input id="idLowY" type="text" size="3" style="width:40px;height:16px;"/> </div> <canvas id="idCanvas" width="800" height ="600" style="background-color: #F0F0F0;float:left;"></canvas> <div style="width:800px;height:22px;float:left;"> <input id="idLowX" type="text" size="3" style="width:40px;height:16px;float:left;"/> <div style="width:704px;height:22px;float:left;"> <label style="color:black;float:left;height:22px;margin-left:5px;">Source Line: <span id="idSourceLine">y = mx + b</span></label> <button style="color:black;height:22px;float:left;margin-left:100px;" onclick="Initialize()">Reset</button> <label style="color:red;float:right;height:22px;margin-right:5px;">Least Squares Line: <span id="idLeastSquaresLine">y = mx + b</span></label> </div> <input id="idHighX" type="text" size="3" style="width:40px;height:16px;float:left;"/> </div> <div style="width:108px;height:22px;float:right;"></div> </div> </body> </html>
class CLine {
#mdaSlope;
#mdIntercept;
// The graph width should be calculated automatically from the data or set by the distribution
constructor(daY) {
let dSumOfX = 0.0;
let dSumOfX2 = 0.0;
let dSumOfY = 0.0;
let dSumOfXY = 0.0;
for (let iX = 0; iX < daY.length; ++iX) {
dSumOfX += iX;
dSumOfX2 += (iX*iX);
dSumOfY += daY[iX];
dSumOfXY += iX*daY[iX];
}
// Denominator = (1.1)(x.x) - (1.x)(1.x)
let dDenominator = (daY.length*dSumOfX2 - dSumOfX*dSumOfX);
// Intercept = (x.x)(1.y) - (1.x)(x.y)/Denominator
this.#mdIntercept = (dSumOfX2*dSumOfY - dSumOfX*dSumOfXY)/dDenominator;
// Slope = (1.1)(x.y) - (1.x)(1.y)/Denominator
this.#mdaSlope =(daY.length*dSumOfXY - dSumOfX*dSumOfY)/dDenominator;
}
CalculateY(dX) {
return dX*this.#mdaSlope + this.#mdIntercept;
}
ToString() {
return "y = "+this.#mdaSlope.toFixed(2)+"x + "+this.#mdIntercept.toFixed(2);
}
}
function GenerateRandomPointFromNormalDistribution(dMean = 0.0, dStdDev = 1.0) {
return dStdDev*InversePhi(Math.random()) + dMean;
}
function InversePhi(p) {
// Constants for the approximation
const p_low = 0.02425;
const p_high = 1 - p_low;
// Coefficients for the rational approximation
const a1 = -3.969683028665376e+01;
const a2 = 2.209460984245205e+02;
const a3 = -2.759285104469687e+02;
const a4 = 1.383577518672690e+02;
const a5 = -3.066479806614716e+01;
const a6 = 2.506628277459239e+00;
const b1 = -5.447609879822406e+01;
const b2 = 1.615858368580409e+02;
const b3 = -1.556989798598866e+02;
const b4 = 6.680131188771972e+01;
const b5 = -1.328068155288572e+01;
const c1 = -7.784894002430293e-03;
const c2 = -3.223964580411365e-01;
const c3 = -2.400758277161838e+00;
const c4 = -2.549732539343734e+00;
const c5 = 4.374664141464968e+00;
const c6 = 2.938163982698783e+00;
const d1 = 7.784695709041462e-03;
const d2 = 3.224671290700398e-01;
const d3 = 2.445134137142996e+00;
const d4 = 3.754408661907416e+00;
let x = 0.0;
if (0.0 < p && p < 1) {
if (p < p_low) {
let q = Math.sqrt(-2*Math.log(p));
x = (((((c1*q+c2)*q+c3)*q+c4)*q+c5)*q+c6) / ((((d1*q+d2)*q+d3)*q+d4)*q+1);
} else if (p_high < p) {
let q = Math.sqrt(-2*Math.log(1-p));
x = -(((((c1*q+c2)*q+c3)*q+c4)*q+c5)*q+c6) / ((((d1*q+d2)*q+d3)*q+d4)*q+1);
} else {
let q = p - 0.5;
let r = q*q;
x = (((((a1*r+a2)*r+a3)*r+a4)*r+a5)*r+a6)*q / (((((b1*r+b2)*r+b3)*r+b4)*r+b5)*r+1);
}
}
return x;
}
function Initialize() {
let qCanvas = document.getElementById("idCanvas");
let qContext2D = qCanvas.getContext("2d");
qContext2D.clearRect(0, 0, 800, 600);
// Transform the coordinates
const kiCanvasW = qContext2D.canvas.width;
const kiCanvasH = qContext2D.canvas.height;
// Reset the transformation so that the flip transformations do not build up an cancel each other.
qContext2D.resetTransform();
// Flip the y-axis and translate by the height
qContext2D.transform(1, 0, 0, -1, 0, kiCanvasH);
const kiGraphW = 16.0;
const kiGraphH = 12.0;
// I will make this transformation myself because this scaling changes the thickness of the lines.
// The range will be 0 to 8 and 0 to 6: [0, 8)x[0 ,6)
//qContext2D.transform(kiCanvasW/kiGraphW, 0, 0, kiCanvasH/kiGraphH, 0, 0);
// This a simple scaling that puts the points in canvas coordinates
const kdScaleW = kiCanvasW/kiGraphW;
const kdScaleH = kiCanvasH/kiGraphH;
// Set the bounds in the graph
let qLowYElement = document.getElementById("idLowY");
qLowYElement.value = 0;
let qHighYElement = document.getElementById("idHighY");
qHighYElement.value = kiGraphH;
let qLowXElement = document.getElementById("idLowX");
qLowXElement.value = 0;
let qHighXElement = document.getElementById("idHighX");
qHighXElement.value = kiGraphW;
// Generate a random line as the source model
// Make the intercept b in [2,3] and the slope m in [0, .5]
const kdIntercept = Math.random() + 2.0;
const kdSlope = .5*Math.random();
let qSourceLineElement = document.getElementById("idSourceLine");
qSourceLineElement.innerHTML = "y = "+kdSlope.toFixed(2)+"x + "+kdIntercept.toFixed(2);
// Graph the source model line
qContext2D.strokeStyle = "black";
qContext2D.lineWidth = 1;
qContext2D.beginPath();
qContext2D.moveTo(0*kdScaleW, kdIntercept*kdScaleH);
qContext2D.lineTo(kiGraphW*kdScaleW, (kdIntercept + kiGraphW*kdSlope)*kdScaleH);
qContext2D.stroke();
// Generate a set of points with a normal distribution to represent the error
let daY = new Array(kiGraphW + 1);
for (let i = 0; i <= kiGraphW; ++i) {
// Graph the position lines
qContext2D.strokeStyle = "gray";
qContext2D.lineWidth = .25;
qContext2D.beginPath();
qContext2D.moveTo(i*kdScaleW, 0*kdScaleH);
qContext2D.lineTo(i*kdScaleW, kiGraphH*kdScaleH);
qContext2D.stroke();
// Calculate the random points
daY[i] = kdIntercept + i*kdSlope + GenerateRandomPointFromNormalDistribution();
}
for (let i = 0; i <= kiGraphH; ++i) {
qContext2D.beginPath();
qContext2D.moveTo(0*kdScaleW, i*kdScaleH);
qContext2D.lineTo(kiGraphW*kdScaleW, i*kdScaleH);
qContext2D.stroke();
}
// Calculate the least squares
let qOLS = new CLine(daY);
let qLeastSquaresLineElement = document.getElementById("idLeastSquaresLine");
qLeastSquaresLineElement.innerHTML = qOLS.ToString();
// Graph the position lines
qContext2D.strokeStyle = "red";
qContext2D.lineWidth = 2;
const kdStartY = qOLS.CalculateY(0);
const kdEndY = qOLS.CalculateY(kiGraphW);
qContext2D.beginPath();
qContext2D.moveTo(0*kdScaleW, kdStartY*kdScaleH);
qContext2D.lineTo(kiGraphW*kdScaleW, kdEndY*kdScaleH);
qContext2D.stroke();
for (let i = 0; i <= kiGraphW; ++i) {
// Draw the error bars
const kdLineY = qOLS.CalculateY(i);
qContext2D.strokeStyle = "red";
qContext2D.lineWidth = 1;
qContext2D.beginPath();
qContext2D.moveTo(i*kdScaleW, kdLineY*kdScaleH);
qContext2D.lineTo(i*kdScaleW, daY[i]*kdScaleH);
qContext2D.stroke();
// Graph the points
qContext2D.fillStyle = "red";
qContext2D.beginPath();
const kdRadius = 5;
qContext2D.ellipse(i*kdScaleW, daY[i]*kdScaleH, kdRadius, kdRadius, 0, 0, 2*Math.PI);
qContext2D.fill();
}
}
© 20072025 XoaX.net LLC. All rights reserved.