node-red-contrib-prib-functions
Version:
Node-RED added node functions.
447 lines (442 loc) • 16.2 kB
JavaScript
const logger = new (require("node-red-contrib-logger"))("Data Analysis");
logger.sendInfo("Copyright 2020 Jaroslav Peter Prib");
require("./autocorrelation.js");
require("./arrayLast");
require("./arrayDifference")
require("./arrayDifferenceSeasonal")
require("./arrayDifferenceSeasonalSecondOrder")
require("./arrayDifferenceSecondOrder")
require("./arrayRandom")
const ed=require("./euclideanDistance.js");
require("./forNestedEach");
function crossNormalisedDeltas() {
const dataPoints=this.dataPoint;
const keys=Object.keys(dataPoints);
const normalisedAvgDelta=keys.reduce((a,c,i)=>a+=dataPoints[c].normalised,0)/keys.length;
return keys.map(c=>{
return {key:c,value:dataPoints[c].normalised-normalisedAvgDelta};
}).sort((a,b)=>a.value-b.value);
}
function predictForKey(node,key) {
const dp=node.dataPoint[key];
if(dp) return new Predictions(dp);
throw Error("no data points");
}
function Predictions(dp) {
const predictions=[];
if(!dp) throw Error("no data points");
const lastValue=dp.value.last(),lastValue2=lastValue*2;
if(dp.delta) {
const delta=dp.delta,
lastDeltaValue=delta.value.last(),
deltaMovingAvg=delta.movingAvg,
deltaWeightedMovingAvg=delta.weightedMovingAvg;
predictions.push(lastValue+lastDeltaValue);
predictions.push(lastValue+deltaMovingAvg);
predictions.push(lastValue+deltaWeightedMovingAvg);
predictions.push(lastValue2-dp.exponentialWeightedMoving[0].movingAverage);
predictions.push(lastValue+delta.exponentialWeightedMoving[0].movingAverage);
predictions.push(lastValue2-dp.exponentialWeightedMoving[1].movingAverage);
predictions.push(lastValue+delta.exponentialWeightedMoving[1].movingAverage);
predictions.push(lastValue2-dp.exponentialWeightedMoving[2].movingAverage);
predictions.push(lastValue+delta.exponentialWeightedMoving[2].movingAverage);
}
this.prediction=predictions;
return this;
}
Predictions.prototype.validate=function(value) {
this.accuracy=this.predictions.map(c=>Math.abs(c-value)/c);
return this;
};
function realtimePredict(d,term,node) {
const m=functions.realtime(d,term,node);
m.predict=predict(m);
return m;
}
function EMA(coefficient=0.5) {
if(coefficient<0 || coefficient>1 ) throw Error("coefficient must be between 0 and 1");
this.factor=(1-coefficient);
this.weightedSum=0;
this.weightedCount=0;
this.movingAverage=0;
return this;
}
EMA.prototype.sample=function(value) {
this.weightedSum=value+this.factor*this.weightedSum;
this.weightedCount=1+this.factor*this.weightedSum;
this.movingAverage=this.weightedSum/this.weightedCount;
return this;
}
function setDataPoint(value,term,node,dataPoint) {
if(logger.active) logger.send({label:"setDataPoint",value:value,term,dataPoint});
if(!dataPoint.values) {
Object.assign(dataPoint,{
values:[],
avg:0,
count:0,
movingSum:0,
movingSumSquared:0,
movingSumCubed:0,
outlier:false,
sum:0,
sumSquared:0,
sumCubed:0,
term:term??node.term,
weightedMovingSum:0,
exponentialWeightedMoving:[new EMA(0.25),new EMA(0.5),new EMA(0.75)]
});
};
const count=++dataPoint.count,values=dataPoint.values;
values.push(value);
const movingTerm=Math.min(values.length,dataPoint.term)
dataPoint.isMaxSize=(values.length>dataPoint.maxSize);
dataPoint.removedMovingValue=(dataPoint.isMaxSize?values[values.length-dataPoint.term]:0);
dataPoint.removedValue=(dataPoint.isMaxSize?values.shift():0);
const removedMovingValue=dataPoint.removedMovingValue;
dataPoint.max=Math.max(dataPoint.max||value,value);
dataPoint.min=Math.min(dataPoint.min||value,value);
dataPoint.range=dataPoint.max-dataPoint.min;
dataPoint.sum+=value;
dataPoint.sumSquared+=Math.pow(value,2);
dataPoint.sumCubed+=Math.pow(value,3);
dataPoint.movingSum+=value-removedMovingValue;
dataPoint.movingSumSquared+=Math.pow(value,2)-Math.pow(removedMovingValue,2);
dataPoint.movingSumCubed+=Math.pow(value,3)-Math.pow(removedMovingValue,3);
dataPoint.avg=dataPoint.sum/count;
const avg=dataPoint.avg;
dataPoint.normalised=dataPoint.range ? (value-avg)/dataPoint.range : 0;
dataPoint.movingAvg=dataPoint.movingSum/movingTerm;
dataPoint.variance=dataPoint.sumSquared/count - Math.pow(avg,2);
dataPoint.stdDev=Math.sqrt(dataPoint.variance);
dataPoint.movingVariance=dataPoint.movingSumSquared/movingTerm - Math.pow(dataPoint.movingAvg,2);
dataPoint.movingStdDev=Math.sqrt(dataPoint.movingVariance);
dataPoint.median=functions.median(values);
dataPoint.standardized=( (value-avg)/dataPoint.stdDev )||0;
dataPoint.movingStandardized=( (value-dataPoint.movingAvg)/dataPoint.movingStdDev )||0;
dataPoint.skewness=(dataPoint.sumCubed-3*avg*dataPoint.variance-Math.pow(avg,3))/dataPoint.variance*dataPoint.stdDev;
dataPoint.movingSkewness=(dataPoint.movingSumCubed-3*dataPoint.movingAvg*dataPoint.movingVariance-Math.pow(dataPoint.movingAvg,3))/dataPoint.movingVariance*dataPoint.stdDev;
dataPoint.outlier=node.outliersFunction(node,dataPoint,value);
dataPoint.weightedMovingSum+=count*value;
dataPoint.weightedMovingAvg=(dataPoint.weightedMovingAvg*2/count)/(count+1);
dataPoint.exponentialWeightedMoving.forEach(c=>c.sample(value));
}
function getColumns(node) {
if(node.columns) {
if(node.columns instanceof Array) return node.columns
return eval("["+node.columns+"]");
}
}
functions={
autocovariance:(d,term,node)=>d.autocovariance(node.lag),
autocorrelation:(d,term,node)=>d.autocorrelation(node.lag),
avg:(d)=>functions.sum(d)/d.length,
arrayMax:(d)=>{ // not tested
let max=[],indices
a.forNestedEach((e,f,l)=>{const i=l[l.length-1];if(max[l]<e) {max=e,indices=l}})
},
covariance: (d)=>{
const means=[],covars=[],dl=d.length,dlminus1=dl-1,N=d[0].length;
d.forEach((e,i)=>{
means.push(e.reduce((a,c)=>a+c)/N);
});
for(let i=0;i<dlminus1;i++){
covars[i]=[];
const di=d[i],v=covars[i],meani=means[i];
for(let j=i+1;j<dl;j++){
const dj=d[j],meanj=means[j];
v[j-1]=di.reduce( (a,c,k)=>a+(c-meani)*(dj[k]-meanj),0)/N;
}
};
if(dl==2) return covars[0];
return covars;
},
corelationship:(d)=>{
const covars=functions.covariance(d);
const stdDev=d.map(c=>functions.stdDev(c));
return covars.map((a)=>a.map((c,i)=>c==null?null:c/(stdDev[i+1]*stdDev[i])));
},
deltas :(d)=>d.map( (c,i)=>c-(d[i-1]||0) ),
deltaNormalised :(d)=>d.map( (c,i)=>(c-(d[i-1]||0)) / (d[i-1]||0) ),
distances: (d,term,node)=>{
if(node.columns) return ed.distances(d,getColumns(node))
return ed.distances(d);
},
distancesMin: (d,term,node)=>ed.minDistances(d,getColumns(node)),
distancesMax: (d,term,node)=>ed.maxDistances(d,getColumns(node)),
difference: (d)=>d.difference(),
differenceSeasonal: (d,term,node)=>d.differenceSeasonal(node.lag),
differenceSeasonalSecondOrder: (d,term,node)=>d.differenceSeasonalSecondOrder(node.lag),
differenceSecondOrder: (d)=>d.differenceSecondOrder(),
max: (d)=> Math.max(...d),
median:(d)=>{
const i=Math.floor(d.length/2);
return d.length%2 ? d[i] : (d[i]+d[i-1])/2;
},
min:(d)=>Math.min(...d),
movingAvgSimple:(d,n)=>{
let avg=0;
return d.map( (c,i,a)=>{ avg= i<=n? avg+(c-avg)/(i+1) : avg+(c-a[i-n])/n; return avg; });
},
movingAvgCumulative :(d)=>{
let avg=0;
return d.map( (c,i)=>{ avg+=(c-avg)/(i+1); return avg; });
},
movingAvgExponential:(d,f)=>{
if(f<0 || f>1) throw Error("factor must be >=0 and <=1");
if(d.length==0) return [];
let s=d[0],fi=1-f;
return d.map( (c)=>{
s=c*f+fi*s;
return s;
});
},
movingAvgWeighted :(d,n)=>{
let SN=0, total=0,numerator=0;
for(let i=1;i<=n;i++) {SN+=i;}
return d.map( (c,i,a)=>{
numerator+=n*c-total;
total+=c-(a[i-n]||0);
return numerator/SN;
});
},
normalize:(d)=>{
const range=functions.range(d);
if(range==0) return d.map(()=>0);
const avg=functions.avg(d),
offset=avg/range;
return d.map(c=>c/range-offset);
},
range:(d)=>Math.max(...d)-Math.min(...d),
pearsonR:(d,term,node)=>{
functions.pearsonRLoad(d,term,node);
functions.pearsonRCalc(d,term,node);
return functions.pearsonRResults(node);
},
pearsonRLoad:(d,term,node)=>{
if(!node.dataPoints) {
node.dataPoints=[];
node.samples=0;
for(let j,i=0; i<node.dataProperties.length; i++) {
node.dataPoints[i]={sum:0,sumSquared:0,sumj:[]};
for(j=i+1; j<node.dataProperties.length; j++) {
node.dataPoints[i].sumj[j]=0;
}
}
}
node.samples++;
for(let i=0; i<node.dataProperties.length; i++) {
const v=d[i];
const dp=node.dataPoints[i];
dp.sum+=v;
dp.sumSquared+=v*v;
for(let j=i+1; j<node.dataProperties.length; j++) {
dp.sumj[j]+=v*d[j];
}
}
},
pearsonRCalc:(d,term,node)=>{
node.pearsonR=[];
for(let dpi,j, i=0; i<node.dataProperties.length; i++) {
dpi=node.dataPoints[i];
for(j=i+1; j<node.dataProperties.length; j++) {
dpj=node.dataPoints[j];
let v = ( node.samples*dpi.sumj[j] - dpi.sum*dpj.sum
)/( (Math.sqrt(node.samples*dpi.sumSquared - Math.pow(dpi.sum,2)))
* (Math.sqrt(node.samples*dpj.sumSquared - Math.pow(dpj.sum,2)))
);
node.pearsonR.push({value:v,i:i,j:j});
}
}
node.pearsonR.sort((a,b)=>a.value>b.value);
},
pearsonRResults:(node)=>{
if(node.hasOwnProperty("pearsonR")) {
return {pearsonR:node.pearsonR,dataPoints:node.dataPoints,samples:node.samples};
}
},
standardize:(d)=>{
const avg=functions.avg(d),
stdDev=functions.stdDev(d);
return d.map( (c)=>(c-avg)/stdDev);
},
stdDev:(d)=>Math.sqrt(functions.variance(d)),
skew:(d)=>{
const avg=functions.avg(d),
variance=functions.sumWithFunction(d,(v)=>Math.pow(v,2)) - Math.pow(avg,2);
return (functions.sumWithFunction(d,(v)=>Math.pow(v,3))
- 3*avg*variance
- Math.pow(avg,3))
/ variance*Math.sqrt(variance);
},
realtimePredict: realtimePredict,
realtime:(d,term,node)=>{
if(!d.key) throw Error("key is null, "+JSON.stringify(d));
if(!d.value) throw Error("value is null "+JSON.stringify(d));
let dp;
if(d.key in node.dataPoint) {
dp=node.dataPoint[d.key];
} else {
dp={key:d.key};
node.dataPoint[d.key]=dp;
}
setDataPoint(d.value,term,node,dp);
if(dp.delta) {
if(dp.values.length>1)
setDataPoint(d.value-dp.values[dp.values.length-2],term,node,dp.delta);
} else {
dp.delta={};
}
if(node.lag>1) {
const vectorSize=dp.values.length
if(dp.lag) {
if(node.lag<=vectorSize){
setDataPoint(d.value-dp.values[vectorSize-node.lag],term,node,dp.lag)
const values=dp.lag.values
if(values.length>1) setDataPoint(values[values.length-1]-values[values.length-2],term,node,dp.lag.delta)
}
} else {
dp.lag={delta:{}}
}
}
return dp;
},
sampleVariance:(d)=>{
const mean=functions.avg(d);
const sum=functions.sumWithFunction(d,(v)=>v-mean)
return sum/(d.length-1)
},
sum:(d)=>d.reduce((p,c)=>p+c),
sumWithFunction:(d,f)=>d.reduce((p,c)=>p+f.apply(this,[c]),0),
variance:(d)=>{ //Var(X) = E (X − E(X))2 = E(X2) − (E(X))2
const n=d.length;
return functions.sumWithFunction(d,(v)=>Math.pow(v,2))/n - Math.pow(functions.avg(d),2);
},
sampleStdDev:(d)=>Math.sqrt(functions.sampleVariance(d)),
sampleVariance:(d)=>{
if(d.length<2)return 0
const mean=functions.avg(d);
const sum=d.reduce((a,e)=>a+Math.pow(e-mean,2),0);
return sum/(d.length-1)
}
}
functions.mean=functions.avg;
module.exports = function (RED) {
function dataAnalysisNode(n) {
RED.nodes.createNode(this, n);
const node=Object.assign(this,
{outliersStdDevs:3,crossNormalisedDeltas:crossNormalisedDeltas.bind(this),lag:1,term:3},
n,
{maxErrorDisplay:10,dataPoint:{}}
);
try{
node.lag=Number(node.lag)
if(Number(node.term)==NaN)throw Error("term not a number, value:"+JSON.stringify(node.term))
node.term=Number(node.term)
node.maxSize=Math.max(node.term,node.lag)
if(functions.hasOwnProperty(node.action)) {
node.actionfunction=functions[node.action];
} else {
throw Error("action not found");
}
switch (node.action) {
case "realtime":
node.outliersStdDevs=Number.parseInt(node.outliersStdDevs,10)||3;
if(![1,2,3].includes(node.outliersStdDevs)) throw Error("outlier std deviation "+node.outliersStdDevs+" not 1,2 or 3");
const outliersFunction=(node.outliersBase||"avg")=="median";
node.log("realtime outliersBase set avg "+outliersFunction);
node.outliersFunction=(outliersFunction
?(node,dp,value)=>{
const standardized=Math.abs(((value-dp.median)/dp.stdDev )||0);
// if(logger.active) logger.send({label:"outlier median",standardized:standardized,outliersStdDevs:node.outliersStdDevs,});
return Math.abs(standardized)>node.outliersStdDevs;
}
:(node,dp,value)=>{
// if(logger.active) logger.send({label:"outlier avg",standardized:dp.standardized,outliersStdDevs:node.outliersStdDevs});
return Math.abs(dp.standardized)>node.outliersStdDevs;
});
node.getDatafunction= "((msg,node)=>{return {key:"+node.keyProperty+",value:"+(node.dataProperty||"msg.payload")+"};})";
break;
case "pearsonR":
node.getDatafunction= "((msg,node)=>{return ["+node.dataProperties.join(',')+"];})";
break;
default:
node.getDatafunction= "((msg,node)=>"+(node.dataProperty||"msg.payload")+")";
}
node.log("get data function: "+node.getDatafunction);
node.getData=eval(node.getDatafunction);
node.status({fill:"green",shape:"ring",text:"Ready"});
} catch(ex) {
if(logger.active) logger.send({label:"initialise error",action:node.action,message:ex.message,stack:ex.stack});
logger.send({label:"initialise error",node:n});
node.error(ex);
node.status({fill:"red",shape:"ring",text:"Invalid setup "+ex.message});
}
node.on("input", function(msg) {
if(msg.topic && msg.topic.startsWith("@")) {
try{
const topic=msg.topic.trim(' ');
if(topic=="@stats") {
switch(node.action) {
case "realtime":
msg.result=node.dataPoint;
break;
case "pearsonR":
msg.result=functions.pearsonRResults(node);
break;
}
node.send([null,msg]);
} else if(topic=="@stats set") {
node.dataPoint=msg.payload;
node.warn(topic);
} else if(topic=="@stats reset") {
node.dataPoint={};
node.warn(topic);
} else if(topic.startsWith("@stats set ")) {
const dataPoint=topic.substring("@stats set ".length);
node.dataPoint[dataPoint]=msg.payload;
node.warn(topic);
} else if(topic.startsWith("@stats reset ")) {
const dataPoint=topic.substring("@stats reset ".length);
delete node.dataPoint[dataPoint];
node.warn(topic);
} else if(topic=="@deltasCrossNormalised") {
msg.payload=node.crossNormalisedDeltas();
node.send([null,msg]);
} else {
throw Error("unknown");
}
} catch(ex) {
if(logger.active) logger.send({label:"error input",action:node.action,message:ex.message,stack:ex.stack});
node.error(msg.topic+" failed "+ex.message);
}
return;
}
try{
const data=node.getData(msg,node);
if(data) msg.result=node.actionfunction.apply(node,[data,node.term,node]);
switch(node.action) {
case "realtime":
if(msg.result.outlier) {
node.send([msg,null,msg]);
return;
}
break;
}
} catch(ex) {
msg.error=ex.message;
if(node.maxErrorDisplay) {
--node.maxErrorDisplay;
logger.send({label:"error input",action:node.action,message:ex.message,stack:ex.stack});
if(node.action=="realtime") {
node.error(node.action+" error: "+ex.message);
} else {
node.error(Array.isArray(node.getData(msg,node))? node.action+" error: "+ex.message : "payload not array");
}
node.status({fill:"red",shape:"ring",text:"error(s)"});
}
}
node.send(msg);
});
}
RED.nodes.registerType(logger.label,dataAnalysisNode);
};