node-red-contrib-prib-functions
Version:
Node-RED added node functions.
546 lines (526 loc) • 18.4 kB
JavaScript
require("./arrayAllRowsSwap");
require("./arrayForEachRange.js");
require("./arrayReduceRange.js");
require("./arrayScale.js");
require("./arraySwap.js");
require("./arraySum.js");
require("./arraySumSquared.js");
const generatedMatrixFunction = require("./generateMatrixFunction");
const generateVectorFunction=require("./generateVectorFunction.js")
function PCA() { //Principal Component Analysis
}
PCA.prototype.rowType=Array;
PCA.prototype.getDeviationMatrix=function(matrix) {
const rows=matrix.length;
const onesMatrix=this.getMatrix(rows,rows,1);
const scaled=this.scale(this.multiply(onesMatrix, matrix),1/rows)
return this.subtract(matrix, scaled);
}
PCA.prototype.getDeviationScores=function(deviation) {
const transposed=this.transpose(deviation)
return this.multiply(transposed, deviation);
}
PCA.prototype.getVarianceCovariance=function(devSumOfSquares, sample) {
return this.scale(devSumOfSquares, 1/devSumOfSquares.length);
}
PCA.prototype.getVarianceCovarianceSample=function(devSumOfSquares) {
const factor=devSumOfSquares.length-1
return this.scale(devSumOfSquares, 1/factor);
}
PCA.prototype.getAdjustedData=function(data, ...vectorObjs){ // reduced after removing some dimensions
const vectors = vectorObjs.map((v)=>v.vector);
const matrixMinusMean = this.getDeviationMatrix(data);
const adjustedData = this.multiply(vectors, this.transpose(matrixMinusMean));
const rows=data.length
const avgData =this.scale(multiply(this.getMatrix(rows,rows,1),data), -1/rows); //NOTE get the averages to add back
return {
adjustedData: adjustedData,
formattedAdjustedData:formatData(adjustedData, 2),
avgData: avgData,
selectedVectors: vectors
};
}
// Get original data set from reduced data set (decompress)
PCA.prototype.getOriginalData=function(adjustedData, vectors, avgData) {
const originalWithoutMean = this.transpose(multiply(transpose(vectors), adjustedData));
const originalWithMean = this.subtract(originalWithoutMean, avgData);
return {
originalData: originalWithMean,
formattedOriginalData: this.formatData(originalWithMean, 2)
}
}
PCA.prototype.getPercentageExplained=function(vectors, ...selected) {
const total = vectors.map((v)=>v.eigenvalue).sum();
const explained = selected.map((v)=>v.eigenvalue).sum();
return (explained / total);
}
PCA.prototype.getEigenVectors=function(data) {
const deviationMatrix=this.getDeviationMatrix(data);
const deviationScores=this.getDeviationScores(deviationMatrix)
const matrix=this.getVarianceCovariance(deviationScores)
const result = this.svd(matrix);
const eigenvectors = result.U;
const eigenvalues = result.S;
return eigenvalues.map((value,i)=>{
return {
eigenvalue:value,
vector:eigenvectors.map((vector,j)=>-vector[i]) //prevent completely negative vectors
}
});
}
PCA.prototype.getTopResult=function(data){
const eigenVectors = this.getEigenVectors(data);
const sorted = eigenVectors.sort((a, b)=>b.eigenvalue-a.eigenvalue);
const selected = sorted[0].vector;
return this.getAdjustedData(data, selected);
}
PCA.prototype.formatData=function(data, precision) {
const factor= Math.pow(10, precision || 2);
return data.map((d,i)=>d.map((n)=>Math.round(n * factor) / factor))
}
PCA.prototype.testIsMatrix=(a)=>{
if(!(a instanceof Array)) throw Error("Not matrix at row level, found type of "+typeof a)
const row=a[0];
if(row instanceof Array) return;
if(row instanceof this.rowType) return;
throw Error("Not matrix at column level, found type of "+typeof a[0])
}
const sumVector1=generateVectorFunction({
code:"returnValue+=vector[index]*matrix[index][col]",
args:["matrix","col"],
returnValue:0
})
const multiplyMatrix=generatedMatrixFunction({
code:"setElement(sumVector1(element,bMatrix,columnOffset));",
args:["bMatrix"],
returnValue:"Object.create(Object.getPrototypeOf(matrix))"
})
PCA.prototype.multiply=function(a, b){
this.testIsMatrix(a);
this.testIsMatrix(b);
const rows=a.length;
if(a[0].length !== b.length) throw Error("Non-conformable matrices, left columns: "+a[0].length+" != right rows "+b.length);
const columns=b[0].length;
const result=this.getMatrix(rows,columns);
for(let ri=0;ri<rows;ri++){
for(let ci=0;ci<columns;ci++){
result[ri][ci]=sumVector1(a[ri],b,ci);
// result[ri][ci]=a[ri].reduce((sum,value,rci)=>sum+value*b[rci][ci],0);
}
}
return result
}
PCA.prototype.subtract=function(a,b) {
if(!(a.length === b.length && a[0].length === b[0].length)) throw Error('Both A and B should have the same dimensions');
const rows=a.length;
const columns=a[0].length;
const result=this.getMatrix(rows,columns);
for(let ri=0;ri<rows;ri++){
for(let ci=0;ci<columns;ci++){
result[ri][ci]=a[ri][ci]-b[ri][ci];
}
}
return result
}
function mapMatrix(matrix,callFunction) {
if(callFunction)
return matrix.map((row,ri)=>row.map((cell,ci)=>callFunction(cell,ri,ci,matrix)));
return this.cloneMatrix(matrix);
};
const mapVector=generateVectorFunction({
code:"vector[index]=fromVector[index]",
args:["fromVector"],
});
const cloneVector=generateVectorFunction({
code:"returnValue[index]=vector[index]",
args:["type=Array"],
});
function cloneMatrix(matrix){
const columns=matrix.length
const rows=matrix[0].length
const result=new Array(rows)
for(let ri=0;ri<rows;ri++){
const row=new this.rowType(columns);
mapVector(row,matrix[ri]);
result[ri]=row;
}
return result;
}
const transposeVector=generateVectorFunction({
code:"vector[index]=fromVector[index][column]",
args:["fromVector","column"],
});
//const mapVector=generateVectorFunction("returnValue[index]=vector[index]",[])
PCA.prototype.map=mapMatrix;
PCA.prototype.cloneMatrix=cloneMatrix;
PCA.prototype.mapTranspose=function(matrix) {
const columns=matrix.length
const rows=matrix[0].length
const result=new this.rowType(rows)
for(let ri=0;ri<rows;ri++){
const row=new this.rowType(columns);
transposeVector(row,matrix,ri)
result[ri]=row;
}
return result;
}
PCA.prototype.transpose=PCA.prototype.mapTranspose
PCA.prototype.forEachRow=function(matrix,callFunction) {
matrix.forEach(row,RowIndex=>callFunction(row,rowIndex,matrix))
return this
}
PCA.prototype.forEachRowColumn=function(matrix,rowIndex,callFunction) {
const row=matrix[rowIndex];
const columns=row[0].length;
for(let columnIndex=0;columnIndex<columns;columnIndex++)
callFunction(row[columnIndex],rowIndex,columnIndex,matrix)
return this
}
PCA.prototype.forEachColumn=function(matrix,columnIndex,callFunction) {
const rows=matrix.length;
for(let rowIndex=0;rowIndex<rows;rowIndex++)
callFunction(matrix[rowIndex][columnIndex],rowIndex,columnIndex,matrix)
return this
}
PCA.prototype.forEachCell=function(matrix,callFunction) {
const rows=matrix.length;
const columns=matrix[0].length;
for(let rowIndex=0;rowIndex<rows;rowIndex++){
const row=matrix[rowIndex];
for(let columnIndex=0;columnIndex<columns;columnIndex++)
callFunction(row[columnIndex],rowIndex,columnIndex,matrix)
}
return this
}
PCA.prototype.forEachSetCell=function(matrix,callFunction) {
const rows=matrix.length;
const columns=matrix[0].length;
for(let rowIndex=0;rowIndex<rows;rowIndex++){
const row=matrix[rowIndex];
for(let columnIndex=0;columnIndex<columns;columnIndex++)
row[columnIndex]=callFunction(rowIndex,columnIndex,cell,row,matrix)
}
return this
}
PCA.prototype.scale=function(matrix,factor){return this.map(matrix,c=>c*factor)};
function getMatrix(rows=2,columns=rows,fill=0) {
const matrix=new Array(rows);
for(let i=0; i<rows; i++) matrix[i]=new this.rowType(columns).fill(fill);
return matrix;
}
PCA.prototype.getMatrix=getMatrix
PCA.prototype.pythag=(a,b)=>{
if(b == 0.0) return a
const a2 = a**2
const b2 = b**2
return a > b ? a * Math.sqrt(1.0 + b2/a2) : b * Math.sqrt(1.0 + a2/b2)
};
PCA.prototype.rep=function(s,v,k=0){
const n=s[k],returnVector=new this.rowType(n).fill(0);
if(k === s.length-1){
returnVector.fill(v,0,n-1)
} else {
const kPlusOne=k+1;
for(let i=n-1;i>=0;i--) returnVector[i]=this.rep(s,v,kPlusOne);
}
return returnVector;
}
module.exports = PCA;
PCA.prototype.getDeterminant=function(matrix){
const result = this.getDecomposed(matrix);
return result.lum.product()*result.toggle;
}
PCA.prototype.getDecomposed=function(matrix){
// Crout's LU decomposition for matrix determinant and inverse
// lum is lower & upper matrix
// perm is row permuations vector
const rows = matrix.length, rowsMinus1 = rows-1;
const perm = new this.rowType(rows).fill(0.0);
let toggle = +1; // even (+1) or odd (-1) row permutatuions
const result=Object.assign([],matrix);
const lum=Object.assign([],matrix);
for(let ri=0; ri<rows; ++ri) perm[ri]=ri
let piv=rows;
for(let j=0; j<rowsMinus1; ++j) {
const lumRowJ=lum[j];
let max = Math.abs(lumRowJ[j]);
for(let ri=j+1; ri<rows; ++ri) { // pivot index
const xrij = Math.abs(lum[ri][j]);
if(xrij > max) {
max = xrij;
piv = ri;
}
}
if(piv != j) {
lum.swap(piv,j)
perm.swap(piv,j);
toggle = -toggle;
}
const xjj = lumRowJ[j];
if(xjj !== 0.0) {
for(let i=j+1; i<rows; ++i) {
const lumRow=lum[i]
const xij = lumRow[j]/xjj;
lumRow[j] = xij;
for(let ci=0; ci<columns; ++ci) lumRow[ci] -= xij*lumRowJ[ci]
}
}
}
return {toggle:toggle,lum:lum,perm:perm}
}
PCA.prototype.getReduced=function(lum, b){
const rows = lum.length, rowsMinus1 = rows-1;
const x=new this.rowType(n).fill(0.0)
let sum;
for(let ri=0; ri<rows; ++ri) x[ri]=b[ri];
for(let ri=1; ri<rows; ++ri) {
const lumRow=lum[ri];
sum=x[ri];
for(let ci=0;ci<columns;ci++) sum-=lumRow[ci]*x[ci];
x[ri]=sum;
}
x[rowsMinus1] /= lum[rowsMinus1][rowsMinus1];
for (let ri=rows-2; ri>0; --ri) {
sum=x[ri];
for(let ci=0;ci<columns;ci++) sum-=lumRow[ci]*x[ci];
x[ri]=sum/lumRow[ri];
}
return x;
}
function svd(matrix) {//singular value decomposition
const eps=this.rowType instanceof Float64Array?
2**-52:
this.rowType instanceof Float32Array?2**-23:Number.EPSILON;
let precision = eps;
const tolerance = 1.e-64;
const itmax = 50;
const rows=matrix.length
const rowOffsetEnd=rows-1;
const columns=matrix[0].length;
const columnOffsetEnd=columns-1;
if (rows < columns) throw "Need more rows than columns"
let temp;
let c= 0;
let i = 0;
let j = 0;
let k = 0;
let l = 0;
const u = this.cloneMatrix(matrix);
const e = new this.rowType(columns).fill(0.0); //vector1
const q = new this.rowType(columns).fill(0.0); //vector2
const v = this.rep([columns, columns], 0);
//Householder's reduction to bidiagonal form
let f = 0.0;
let g = 0.0;
let h = 0.0;
let x = 0.0;
let y = 0.0;
let z = 0.0;
let s = 0.0;
for(i=0; i<columns; i++) {
e[i] = g; //vector
const uRow=u[i]
const iPlus1=i+1;
const sum=u.reduceRange(i,rowOffsetEnd,(previousValue,row)=>previousValue+row[i]**2)
if(sum <= tolerance)
g = 0.0;
else {
f = uRow[i];
g = Math.sqrt(sum)*(f<0?1:-1);
h = f * g - sum
uRow[i] = f - g;
for(j=iPlus1; j<columns; j++) {
const factor=u.reduceRange(i,rowOffsetEnd,(previousValue,row)=>previousValue+row[i]*row[j])/h;
for(let ri=i;ri<=rowOffsetEnd;ri++) u[ri][j]+=factor*u[ri][i]
}
}
q[i] = g
const sumCols=u.reduceRange(iPlus1,columnOffsetEnd,(previousValue,cell,ci)=>previousValue+u[i][ci]**2)
if(sumCols <= tolerance)
g = 0.0
else {
f = uRow[iPlus1]
g = Math.sqrt(sumCols) * (f<0? 1:-1)
h = f * g - sumCols
uRow[iPlus1] = f - g;
for(let ci=iPlus1;ci<=columnOffsetEnd;ci++) e[ci]=uRow[ci]/h;
for(j = iPlus1; j < rows; j++) {
const uj=u[j];
const sum=uj.reduceRange(iPlus1,columnOffsetEnd,(previousValue,column,ci)=>previousValue+column*uRow[ci]);
for(let ci=iPlus1;ci<=columnOffsetEnd;ci++) uj[ci]+=sum*e[ci]; }
}
y = Math.abs(q[i]) + Math.abs(e[i])
if(y>x) x=y
}
l=columns;
// accumulation of right hand transformations
for(i=columnOffsetEnd; i != -1; i += -1) {
if (g != 0.0) {
const uRow=u[i]
h = g * uRow[i+1]
for(j=l; j<columns; j++)
v[j][i] = uRow[j] / h //u is array, v is square of columns
for(j=l; j < columns; j++) {
const sum=v.reduceRange(l,columnOffsetEnd,(previousValue,row,ri)=>previousValue+u[i][ri] * row[j])
for(let ci=l;ci<columns;ci++){
const vRow=v[ci];
vRow[j]+=vRow[i]*sum
}
}
}
const vRow=v[i]
for(j=l; j<columns; j++) {
vRow[j] = 0;
v[j][i] = 0;
}
vRow[i] = 1;
g = e[i]
l = i
}
// accumulation of left hand transformations
for (i = columns - 1; i != -1; i += -1) {
l = i + 1
g = q[i]
const uRow=u[i];
for(j=l; j<columns; j++) uRow[j]=0;
if (g != 0.0)
for(let i=0;i<rows;i++) u[i][i];
else {
h = uRow[i] * g
for (j = l; j < columns; j++) {
const factor=u.reduceRange(l,rowOffsetEnd,(previousValue,row)=>previousValue+row[i]*row[j])/h;
for(let ci=i;ci<=rowOffsetEnd;ci++){
const row=u[ci];
row[j]+=row[i]*factor
}
}
for(let ri=i;ri<=rowOffsetEnd;ri++) u[ri][i]/=g;
}
uRow[i]++;
}
// diagonalization of the bidiagonal form
precision = precision * x;
for (k = columns - 1; k != -1; k += -1) {
for (var iteration = 0; iteration < itmax; iteration++) { // test f splitting
let test_convergence = false
for (l=k; l>=0; l--) {
if (Math.abs(e[l]) <= precision) {
test_convergence = true
break
}
if (Math.abs(q[l - 1]) <= precision)
break
}
if (!test_convergence) { // cancellation of e[l] if l>0
c = 0.0
s = 1.0
const l1 = l - 1
for(i=l; i<=k; i++) {
const ei=e[i];
f = s * ei
e[i] = c * ei
if (Math.abs(f) <= precision)
break
g = q[i]
h = this.pythag(f, g)
q[i] = h
c = g / h
s = -f / h
for(let ci=l;ci<=columnOffsetEnd;ci++){
const row=u[ci];
const cl1 = row[l1]
const ci = row[i]
row[l1] = cl1 * c + (ci * s)
row[i] = -cl1 * s + (ci * c)
}
}
}
// test f convergence
z = q[k]
if(l == k) { //convergence
if(z < 0.0) { //q[k] is made non-negative
q[k] = -z
for(let ci=l;ci<=columnOffsetEnd;ci++) v[ci][k]*=-1;
}
break //break out of iteration loop and move on to next k value
}
if(iteration >= itmax - 1)
throw 'Error: no convergence.'
// shift from bottom 2x2 minor
x = q[l]
const kMinus1=k-1;
y = q[kMinus1]
g = e[kMinus1]
h = e[k]
const z2=z**2
f = ((y**2 - z2 + g**2 - h*h)) / (2.0 * h * y)
g = this.pythag(f, 1.0)
const fg=f+f< 0.0?-g:g;
f = (x**2 - z2 + h * (y / fg - h)) / x
// next QR transformation
c = 1.0
s = 1.0
for(i=l+1; i <= k; i++) {
const iMinus1=i-1;
g = e[i]
y = q[i]
h = s * g
g = c * g
z = this.pythag(f, h)
e[iMinus1] = z
c = f / z
s = h / z
f = x * c + g * s
g = -x * s + g * c
h = y * s
y = y * c
for(j=0; j<columns; j++) {
const row=v[j];
x = row[iMinus1]
z = row[i]
row[iMinus1] = x * c + z * s
row[i] = -x * s + z * c
}
z = this.pythag(f, h)
q[iMinus1] = z
c = f / z
s = h / z
f = c * g + s * y
x = -s * g + c * y
for(j=0; j<rows; j++) {
const row=u[j];
y = row[iMinus1]
z = row[i]
row[iMinus1] = y * c + z * s
row[i] = -y * s + z * c
}
}
e[l] = 0.0
e[k] = f
q[k] = x
}
}
const ql=q.length;
for(i=0; i<ql; i++) if(q[i]<precision) q[i]=0;
//sort eigenvalues
for(i=0; i<columns; i++) {
for(j=i-1; j>=0; j--) {
const qj=q[j];
const qi=q[i]
if(qj < qi) {
c = qj
q[j] = qi
q[i] = c
u.allRowsSwap(i,j);
v.allRowsSwap(i,j);
i=j
}
}
}
return {
U: u,
S: q,
V: v
}
}
PCA.prototype.svd=svd;