als-statistics
Version:
A powerful and lightweight JavaScript library for descriptive statistics, regression, clustering, outlier detection, and noise analysis using a flexible table/column architecture.
106 lines (90 loc) • 4.17 kB
JavaScript
const { tCDF } = require('../cdf');
const MatrixUtils = require('./matrix-utils');
class LinearRegression {
constructor(table, yName, xNames = []) {
this.table = table;
this.yName = yName;
this.xNames = xNames
this._originalX = xNames.length > 0 ? xNames : Object.keys(table.columns).filter(k => k !== yName);
this._mediator = null;
this._moderator = null;
this._interactionName = null;
}
mediator(name) { this._mediator = name; return this }
moderator(name) { this._moderator = name; return this }
calculate() {
const fullCols = { ...this.table.columns };
this.xNames = [...this._originalX];
if (this._mediator && !this.xNames.includes(this._mediator)) this.xNames.push(this._mediator); // Медиатор
if (this._moderator) {
const x = this.table.columns[this._originalX[0]].values;
const z = this.table.columns[this._moderator].values;
this._interactionName = `${this._originalX[0]}*${this._moderator}`;
fullCols[this._interactionName] = { values: x.map((xi, i) => xi * z[i]) };
if (!this.xNames.includes(this._moderator)) this.xNames.push(this._moderator);
this.xNames.push(this._interactionName);
}
this.y = this.table.columns[this.yName].values;
this.X = fullCols[this.xNames[0]].values.map((_, i) => [1, ...this.xNames.map(name => fullCols[name].values[i])]);
this.n = this.X.length;
this.k = this.X[0].length;
this.coefficients = this.computeCoefficients();
if (this.coefficients.some(c => !isFinite(c))) throw new Error("Regression failed: singular matrix or constant predictors");
this.yHat = this.predict(this.X);
this.residuals = this.y.map((yi, i) => yi - this.yHat[i]);
this.r2 = this.computeR2();
this.standardErrors = this.computeStandardErrors();
this.pValues = this.computePValues();
return this;
}
computeCoefficients() {
const XT = MatrixUtils.transpose(this.X);
const XTX = MatrixUtils.multiply(XT, this.X);
const XTy = MatrixUtils.multiplyVec(XT, this.y);
return MatrixUtils.solve ? MatrixUtils.solve(XTX, XTy) : MatrixUtils.multiplyVec(MatrixUtils.inverse(XTX), XTy);
}
predict(X) { return X.map(row => row.reduce((acc, val, j) => acc + val * this.coefficients[j], 0)) }
computeR2() {
const yMean = this.y.reduce((a, b) => a + b, 0) / this.n;
const ssTot = this.y.reduce((acc, yi) => acc + (yi - yMean) ** 2, 0);
const ssRes = this.residuals.reduce((acc, e) => acc + e ** 2, 0);
return 1 - ssRes / ssTot;
}
computeStandardErrors() {
const XT = MatrixUtils.transpose(this.X);
const XTX = MatrixUtils.multiply(XT, this.X);
const invXTX = MatrixUtils.inverse(XTX);
const mse = this.residuals.reduce((acc, e) => acc + e ** 2, 0) / (this.n - this.k);
return invXTX.map((row, i) => Math.sqrt(row[i] * mse));
}
computePValues() {
return this.standardErrors.map((se, i) => {
const t = this.coefficients[i] / se;
return 2 * (1 - tCDF(Math.abs(t), this.n - this.k));
});
}
get result() {
if(!this.xNames) this.calculate()
return {
Variable: ['Intercept', ...this.xNames],
Coefficient: this.coefficients,
StdError: this.standardErrors,
pValue: this.pValues
};
}
get htmlTable() {
const { Variable, Coefficient, StdError, pValue } = this.result
return /*html*/`<table>
<thead><tr style="text-align:left;">${['Variable', 'Coefficient', 'StdError', 'pValue'].map(v => /*html*/`<th>${v}</th>`).join('')}</tr></thead>
<tbody>
${Variable.map((vName, i) => /*html*/`<tr>
<th style="text-align:left;">${vName}</th>
<td>${Coefficient[i].toFixed(3)}</td>
<td>${StdError[i].toFixed(3)}</td>
<td>${pValue[i].toFixed(5)}</td>
</tr>`).join('')}
</tbody>
</table>`
}
}
module.exports = LinearRegression