sitespeed-grade
Version:
Sitespeed grade.
610 lines (505 loc) • 14.7 kB
JavaScript
/*
Note that if your data is too large, there _will_ be overflow.
*/
function asc(a, b) { return a-b; }
var config_params = {
bucket_precision: function(o, s) {
if(typeof s != "number" || s <= 0) {
throw new Error("bucket_precision must be a positive number");
}
o._config.bucket_precision = s;
o.buckets = [];
},
buckets: function(o, b) {
if(!Array.isArray(b) || b.length == 0) {
throw new Error("buckets must be an array of bucket limits");
}
o._config.buckets = b;
o.buckets = [];
},
bucket_extension_interval: function(o, s) {
if(s === undefined)
return;
if(typeof s != "number" || s<=0) {
throw new Error("bucket_extension_interval must be a positive number");
}
o._config.bucket_extension_interval = s;
},
store_data: function(o, s) {
if(typeof s != "boolean") {
throw new Error("store_data must be a true or false");
}
o._config.store_data = s;
},
sampling: function(o, s) {
if(typeof s != "boolean") {
throw new Error("sampling must be a true or false");
}
o._config.sampling = s;
}
};
function Stats(c) {
this._config = { store_data: true };
if(c) {
for(var k in config_params) {
if(c.hasOwnProperty(k)) {
config_params[k](this, c[k]);
}
}
}
this.reset();
return this;
}
Stats.prototype = {
reset: function() {
if(this._config.store_data)
this.data = [];
this.length = 0;
this.sum = 0;
this.sum_of_squares = 0;
this.sum_of_logs = 0;
this.sum_of_square_of_logs = 0;
this.zeroes = 0;
this.max = this.min = null;
this._reset_cache();
return this;
},
_reset_cache: function() {
this._stddev = null;
if(this._config.store_data)
this._data_sorted = null;
},
_find_bucket: function(a) {
var b=0, e, l;
if(this._config.buckets) {
l = this._config.buckets.length;
if(this._config.bucket_extension_interval && a >= this._config.buckets[l-1]) {
e=a-this._config.buckets[l-1];
b = parseInt(e/this._config.bucket_extension_interval) + l;
if(this._config.buckets[b] === undefined)
this._config.buckets[b] = this._config.buckets[l-1] + (parseInt(e/this._config.bucket_extension_interval)+1)*this._config.bucket_extension_interval;
if(this._config.buckets[b-1] === undefined)
this._config.buckets[b-1] = this._config.buckets[l-1] + parseInt(e/this._config.bucket_extension_interval)*this._config.bucket_extension_interval;
}
for(; b<l; b++) {
if(a < this._config.buckets[b]) {
break;
}
}
}
else if(this._config.bucket_precision) {
b = Math.floor(a/this._config.bucket_precision);
}
return b;
},
_add_cache: function(a) {
var tuple=[1], i;
if(a instanceof Array) {
tuple = a;
a = tuple.shift();
}
this.sum += a*tuple[0];
this.sum_of_squares += a*a*tuple[0];
if(a === 0) {
this.zeroes++;
}
else {
this.sum_of_logs += Math.log(a)*tuple[0];
this.sum_of_square_of_logs += Math.pow(Math.log(a), 2)*tuple[0];
}
this.length += tuple[0];
if(tuple[0] > 0) {
if(this.max === null || this.max < a)
this.max = a;
if(this.min === null || this.min > a)
this.min = a;
}
if(this.buckets) {
var b = this._find_bucket(a);
if(!this.buckets[b])
this.buckets[b] = [0];
this.buckets[b][0] += tuple.shift();
for(i=0; i<tuple.length; i++)
this.buckets[b][i+1] = (this.buckets[b][i+1]|0) + (tuple[i]|0);
}
this._reset_cache();
},
_del_cache: function(a) {
var tuple=[1], i;
if(a instanceof Array) {
tuple = a;
a = tuple.shift();
}
this.sum -= a*tuple[0];
this.sum_of_squares -= a*a*tuple[0];
if(a === 0) {
this.zeroes--;
}
else {
this.sum_of_logs -= Math.log(a)*tuple[0];
this.sum_of_square_of_logs -= Math.pow(Math.log(a), 2)*tuple[0];
}
this.length -= tuple[0];
if(this._config.store_data) {
if(this.length === 0) {
this.max = this.min = null;
}
if(this.length === 1) {
this.max = this.min = this.data[0];
}
else if(tuple[0] > 0 && (this.max === a || this.min === a)) {
var i = this.length-1;
if(i>=0) {
this.max = this.min = this.data[i--];
while(i-- >= 0) {
if(this.max < this.data[i])
this.max = this.data[i];
if(this.min > this.data[i])
this.min = this.data[i];
}
}
}
}
if(this.buckets) {
var b=this._find_bucket(a);
if(this.buckets[b]) {
this.buckets[b][0] -= tuple.shift();
if(this.buckets[b][0] === 0)
delete this.buckets[b];
else
for(i=0; i<tuple.length; i++)
this.buckets[b][i+1] = (this.buckets[b][i+1]|0) - (tuple[i]|0);
}
}
this._reset_cache();
},
push: function() {
var i, a, args=Array.prototype.slice.call(arguments, 0);
if(args.length && args[0] instanceof Array)
args = args[0];
for(i=0; i<args.length; i++) {
a = args[i];
if(this._config.store_data)
this.data.push(a);
this._add_cache(a);
}
return this;
},
push_tuple: function(tuple) {
if(!this.buckets) {
throw new Error("push_tuple is only valid when using buckets");
}
this._add_cache(tuple);
},
pop: function() {
if(this.length === 0 || this._config.store_data === false)
return undefined;
var a = this.data.pop();
this._del_cache(a);
return a;
},
remove_tuple: function(tuple) {
if(!this.buckets) {
throw new Error("remove_tuple is only valid when using buckets");
}
this._del_cache(tuple);
},
reset_tuples: function(tuple) {
var b, l, t, ts=tuple.length;
if(!this.buckets) {
throw new Error("reset_tuple is only valid when using buckets");
}
for(b=0, l=this.buckets.length; b<l; b++) {
if(!this.buckets[b] || this.buckets[b].length <= 1) {
continue;
}
for(t=0; t<ts; t++) {
if(typeof tuple[t] !== 'undefined') {
this.buckets[b][t] = tuple[t];
}
}
}
},
unshift: function() {
var i, a, args=Array.prototype.slice.call(arguments, 0);
if(args.length && args[0] instanceof Array)
args = args[0];
i=args.length;
while(i--) {
a = args[i];
if(this._config.store_data)
this.data.unshift(a);
this._add_cache(a);
}
return this;
},
shift: function() {
if(this.length === 0 || this._config.store_data === false)
return undefined;
var a = this.data.shift();
this._del_cache(a);
return a;
},
amean: function() {
if(this.length === 0)
return NaN;
return this.sum/this.length;
},
gmean: function() {
if(this.length === 0)
return NaN;
if(this.zeroes > 0)
return NaN;
return Math.exp(this.sum_of_logs/this.length);
},
stddev: function() {
if(this.length === 0)
return NaN;
var n=this.length;
if(this._config.sampling)
n--;
if(this._stddev === null)
this._stddev = Math.sqrt(Math.max(0, this.length * this.sum_of_squares - this.sum*this.sum)/(this.length*n));
return this._stddev;
},
gstddev: function() {
if(this.length === 0)
return NaN;
if(this.zeroes > 0)
return NaN;
var n=this.length;
if(this._config.sampling)
n--;
return Math.exp(Math.sqrt((this.length * this.sum_of_square_of_logs - this.sum_of_logs*this.sum_of_logs)/(this.length*n)));
},
moe: function() {
if(this.length === 0)
return NaN;
// see http://en.wikipedia.org/wiki/Standard_error_%28statistics%29
return 1.96*this.stddev()/Math.sqrt(this.length);
},
range: function() {
if(this.length === 0)
return [NaN, NaN];
return [this.min, this.max];
},
distribution: function() {
if(this.length === 0)
return [];
if(!this.buckets)
throw new Error("bucket_precision or buckets not configured.");
var d=[], i, j, k, l;
if(this._config.buckets) {
j=this.min;
l=Math.min(this.buckets.length, this._config.buckets.length);
for(i=0; i<l; j=this._config.buckets[i++]) { // this has to be i++ and not ++i
if(this._config.buckets[i] === undefined && this._config.bucket_extension_interval)
this._config.buckets[i] = this._config.buckets[i-1] + this._config.bucket_extension_interval;
if(this.min > this._config.buckets[i])
continue;
d[i] = {
bucket: (j+this._config.buckets[i])/2,
range: [j, this._config.buckets[i]],
count: (this.buckets[i]?this.buckets[i][0]:0),
tuple: this.buckets[i]?this.buckets[i].slice(1):[]
};
if(this.max < this._config.buckets[i])
break;
}
if(i == l && this.buckets[i]) {
d[i] = {
bucket: (j + this.max)/2,
range: [j, this.max],
count: this.buckets[i][0],
tuple: this.buckets[i]?this.buckets[i].slice(1):[]
};
}
}
else if(this._config.bucket_precision) {
i=Math.floor(this.min/this._config.bucket_precision);
l=Math.floor(this.max/this._config.bucket_precision)+1;
for(j=0; i<l && i<this.buckets.length; i++, j++) {
if(!this.buckets[i]) {
continue;
}
d[j] = {
bucket: (i+0.5)*this._config.bucket_precision,
range: [i*this._config.bucket_precision, (i+1)*this._config.bucket_precision],
count: this.buckets[i][0],
tuple: this.buckets[i]?this.buckets[i].slice(1):[]
};
}
}
return d;
},
percentile: function(p) {
if(this.length === 0 || (!this._config.store_data && !this.buckets))
return NaN;
// If we come here, we either have sorted data or sorted buckets
var v;
if(p <= 0)
v=0;
else if(p == 25)
v = [Math.floor((this.length-1)*0.25), Math.ceil((this.length-1)*0.25)];
else if(p == 50)
v = [Math.floor((this.length-1)*0.5), Math.ceil((this.length-1)*0.5)];
else if(p == 75)
v = [Math.floor((this.length-1)*0.75), Math.ceil((this.length-1)*0.75)];
else if(p >= 100)
v = this.length-1;
else
v = Math.floor(this.length*p/100);
if(v === 0)
return this.min;
if(v === this.length-1)
return this.max;
if(this._config.store_data) {
if(this._data_sorted === null)
this._data_sorted = this.data.slice(0).sort(asc);
if(typeof v == 'number')
return this._data_sorted[v];
else
return (this._data_sorted[v[0]] + this._data_sorted[v[1]])/2;
}
else {
var j;
if(typeof v != 'number')
v = (v[0]+v[1])/2;
if(this._config.buckets)
j=0;
else if(this._config.bucket_precision)
j = Math.floor(this.min/this._config.bucket_precision);
for(; j<this.buckets.length; j++) {
if(!this.buckets[j])
continue;
if(v<=this.buckets[j][0]) {
break;
}
v-=this.buckets[j][0];
}
return this._get_nth_in_bucket(v, j);
}
},
_get_nth_in_bucket: function(n, b) {
var range = [];
if(this._config.buckets) {
range[0] = (b>0?this._config.buckets[b-1]:this.min);
range[1] = (b<this._config.buckets.length?this._config.buckets[b]:this.max);
}
else if(this._config.bucket_precision) {
range[0] = Math.max(b*this._config.bucket_precision, this.min);
range[1] = Math.min((b+1)*this._config.bucket_precision, this.max);
}
return range[0] + (range[1] - range[0])*n/this.buckets[b][0];
},
median: function() {
return this.percentile(50);
},
iqr: function() {
var q1, q3, fw;
q1 = this.percentile(25);
q3 = this.percentile(75);
fw = (q3-q1)*1.5;
return this.band_pass(q1-fw, q3+fw, true);
},
band_pass: function(low, high, open, config) {
var i, j, b, b_val, i_val;
if(!config)
config = this._config;
b = new Stats(config);
if(this.length === 0)
return b;
if(this._config.store_data) {
if(this._data_sorted === null)
this._data_sorted = this.data.slice(0).sort(asc);
for(i=0; i<this.length && (this._data_sorted[i] < high || (!open && this._data_sorted[i] === high)); i++) {
if(this._data_sorted[i] > low || (!open && this._data_sorted[i] === low)) {
b.push(this._data_sorted[i]);
}
}
}
else if(this._config.buckets) {
for(i=0; i<=this._config.buckets.length; i++) {
if(this._config.buckets[i] < this.min)
continue;
b_val = (i==0?this.min:this._config.buckets[i-1]);
if(b_val < this.min)
b_val = this.min;
if(b_val > this.max)
b_val = this.max;
if(high < b_val || (open && high === b_val)) {
break;
}
if(low < b_val || (!open && low === b_val)) {
for(j=0; j<(this.buckets[i]?this.buckets[i][0]:0); j++) {
i_val = Stats.prototype._get_nth_in_bucket.call(this, j, i);
if( (i_val > low || (!open && i_val === low))
&& (i_val < high || (!open && i_val === high))
) {
b.push(i_val);
}
}
}
}
b.min = Math.max(low, b.min);
b.max = Math.min(high, b.max);
}
else if(this._config.bucket_precision) {
var low_i = Math.floor(low/this._config.bucket_precision),
high_i = Math.floor(high/this._config.bucket_precision)+1;
for(i=low_i; i<Math.min(this.buckets.length, high_i); i++) {
for(j=0; j<(this.buckets[i]?this.buckets[i][0]:0); j++) {
i_val = Stats.prototype._get_nth_in_bucket.call(this, j, i);
if( (i_val > low || (!open && i_val === low))
&& (i_val < high || (!open && i_val === high))
) {
b.push(i_val);
}
}
}
b.min = Math.max(low, b.min);
b.max = Math.min(high, b.max);
}
return b;
},
copy: function(config) {
var b = Stats.prototype.band_pass.call(this, this.min, this.max, false, config);
b.sum = this.sum;
b.sum_of_squares = this.sum_of_squares;
b.sum_of_logs = this.sum_of_logs;
b.sum_of_square_of_logs = this.sum_of_square_of_logs;
b.zeroes = this.zeroes;
return b;
},
Σ: function() {
return this.sum;
},
Π: function() {
return this.zeroes > 0 ? 0 : Math.exp(this.sum_of_logs);
}
};
Stats.prototype.σ=Stats.prototype.stddev;
Stats.prototype.μ=Stats.prototype.amean;
exports.Stats = Stats;
if(process.argv[1] && process.argv[1].match(__filename)) {
var s = new Stats({store_data:false, buckets: [ 1, 5, 10, 15, 20, 25, 30, 35 ]}).push(1, 2, 3);
var l = process.argv.slice(2);
if(!l.length) l = [10, 11, 15, 8, 13, 12, 19, 32, 17, 16];
l.forEach(function(e, i, a) { a[i] = parseFloat(e, 10); });
Stats.prototype.push.apply(s, l);
console.log(s.data);
console.log(s.amean().toFixed(2), s.μ().toFixed(2), s.stddev().toFixed(2), s.σ().toFixed(2), s.gmean().toFixed(2), s.median().toFixed(2), s.moe().toFixed(2), s.distribution());
var t=s.copy({buckets: [0, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 25, 30, 35] });
console.log(t.amean().toFixed(2), t.μ().toFixed(2), t.stddev().toFixed(2), t.σ().toFixed(2), t.gmean().toFixed(2), t.median().toFixed(2), t.moe().toFixed(2), t.distribution());
s = new Stats({store_data: false, buckets: [1, 5, 10, 15, 20, 25, 30, 35]});
s.push_tuple([1, 1, 3, 4]);
s.push_tuple([2, 1, 5, 8]);
s.push_tuple([3, 1, 4, 9]);
s.push_tuple([1, 1, 13, 14]);
console.log(s.amean(), s.median());
console.log(s.distribution());
s.remove_tuple([1, 1, 3, 4]);
s.push_tuple([4, 1, 3, 3]);
console.log(s.amean(), s.median());
console.log(s.distribution());
}