jstat
Version:
Statistical Library for JavaScript
686 lines (549 loc) • 16.6 kB
JavaScript
var jStat = (function(Math, undefined) {
// For quick reference.
var concat = Array.prototype.concat;
var slice = Array.prototype.slice;
var toString = Object.prototype.toString;
// Calculate correction for IEEE error
// TODO: This calculation can be improved.
function calcRdx(n, m) {
var val = n > m ? n : m;
return Math.pow(10,
17 - ~~(Math.log(((val > 0) ? val : -val)) * Math.LOG10E));
}
var isArray = Array.isArray || function isArray(arg) {
return toString.call(arg) === '[object Array]';
};
function isFunction(arg) {
return toString.call(arg) === '[object Function]';
}
function isNumber(num) {
return (typeof num === 'number') ? num - num === 0 : false;
}
// Converts the jStat matrix to vector.
function toVector(arr) {
return concat.apply([], arr);
}
// The one and only jStat constructor.
function jStat() {
return new jStat._init(arguments);
}
// TODO: Remove after all references in src files have been removed.
jStat.fn = jStat.prototype;
// By separating the initializer from the constructor it's easier to handle
// always returning a new instance whether "new" was used or not.
jStat._init = function _init(args) {
// If first argument is an array, must be vector or matrix.
if (isArray(args[0])) {
// Check if matrix.
if (isArray(args[0][0])) {
// See if a mapping function was also passed.
if (isFunction(args[1]))
args[0] = jStat.map(args[0], args[1]);
// Iterate over each is faster than this.push.apply(this, args[0].
for (var i = 0; i < args[0].length; i++)
this[i] = args[0][i];
this.length = args[0].length;
// Otherwise must be a vector.
} else {
this[0] = isFunction(args[1]) ? jStat.map(args[0], args[1]) : args[0];
this.length = 1;
}
// If first argument is number, assume creation of sequence.
} else if (isNumber(args[0])) {
this[0] = jStat.seq.apply(null, args);
this.length = 1;
// Handle case when jStat object is passed to jStat.
} else if (args[0] instanceof jStat) {
// Duplicate the object and pass it back.
return jStat(args[0].toArray());
// Unexpected argument value, return empty jStat object.
// TODO: This is strange behavior. Shouldn't this throw or some such to let
// the user know they had bad arguments?
} else {
this[0] = [];
this.length = 1;
}
return this;
};
jStat._init.prototype = jStat.prototype;
jStat._init.constructor = jStat;
// Utility functions.
// TODO: for internal use only?
jStat.utils = {
calcRdx: calcRdx,
isArray: isArray,
isFunction: isFunction,
isNumber: isNumber,
toVector: toVector
};
jStat._random_fn = Math.random;
jStat.setRandom = function setRandom(fn) {
if (typeof fn !== 'function')
throw new TypeError('fn is not a function');
jStat._random_fn = fn;
};
// Easily extend the jStat object.
// TODO: is this seriously necessary?
jStat.extend = function extend(obj) {
var i, j;
if (arguments.length === 1) {
for (j in obj)
jStat[j] = obj[j];
return this;
}
for (i = 1; i < arguments.length; i++) {
for (j in arguments[i])
obj[j] = arguments[i][j];
}
return obj;
};
// Returns the number of rows in the matrix.
jStat.rows = function rows(arr) {
return arr.length || 1;
};
// Returns the number of columns in the matrix.
jStat.cols = function cols(arr) {
return arr[0].length || 1;
};
// Returns the dimensions of the object { rows: i, cols: j }
jStat.dimensions = function dimensions(arr) {
return {
rows: jStat.rows(arr),
cols: jStat.cols(arr)
};
};
// Returns a specified row as a vector or return a sub matrix by pick some rows
jStat.row = function row(arr, index) {
if (isArray(index)) {
return index.map(function(i) {
return jStat.row(arr, i);
})
}
return arr[index];
};
// return row as array
// rowa([[1,2],[3,4]],0) -> [1,2]
jStat.rowa = function rowa(arr, i) {
return jStat.row(arr, i);
};
// Returns the specified column as a vector or return a sub matrix by pick some
// columns
jStat.col = function col(arr, index) {
if (isArray(index)) {
var submat = jStat.arange(arr.length).map(function() {
return new Array(index.length);
});
index.forEach(function(ind, i){
jStat.arange(arr.length).forEach(function(j) {
submat[j][i] = arr[j][ind];
});
});
return submat;
}
var column = new Array(arr.length);
for (var i = 0; i < arr.length; i++)
column[i] = [arr[i][index]];
return column;
};
// return column as array
// cola([[1,2],[3,4]],0) -> [1,3]
jStat.cola = function cola(arr, i) {
return jStat.col(arr, i).map(function(a){ return a[0] });
};
// Returns the diagonal of the matrix
jStat.diag = function diag(arr) {
var nrow = jStat.rows(arr);
var res = new Array(nrow);
for (var row = 0; row < nrow; row++)
res[row] = [arr[row][row]];
return res;
};
// Returns the anti-diagonal of the matrix
jStat.antidiag = function antidiag(arr) {
var nrow = jStat.rows(arr) - 1;
var res = new Array(nrow);
for (var i = 0; nrow >= 0; nrow--, i++)
res[i] = [arr[i][nrow]];
return res;
};
// Transpose a matrix or array.
jStat.transpose = function transpose(arr) {
var obj = [];
var objArr, rows, cols, j, i;
// Make sure arr is in matrix format.
if (!isArray(arr[0]))
arr = [arr];
rows = arr.length;
cols = arr[0].length;
for (i = 0; i < cols; i++) {
objArr = new Array(rows);
for (j = 0; j < rows; j++)
objArr[j] = arr[j][i];
obj.push(objArr);
}
// If obj is vector, return only single array.
return obj.length === 1 ? obj[0] : obj;
};
// Map a function to an array or array of arrays.
// "toAlter" is an internal variable.
jStat.map = function map(arr, func, toAlter) {
var row, nrow, ncol, res, col;
if (!isArray(arr[0]))
arr = [arr];
nrow = arr.length;
ncol = arr[0].length;
res = toAlter ? arr : new Array(nrow);
for (row = 0; row < nrow; row++) {
// if the row doesn't exist, create it
if (!res[row])
res[row] = new Array(ncol);
for (col = 0; col < ncol; col++)
res[row][col] = func(arr[row][col], row, col);
}
return res.length === 1 ? res[0] : res;
};
// Cumulatively combine the elements of an array or array of arrays using a function.
jStat.cumreduce = function cumreduce(arr, func, toAlter) {
var row, nrow, ncol, res, col;
if (!isArray(arr[0]))
arr = [arr];
nrow = arr.length;
ncol = arr[0].length;
res = toAlter ? arr : new Array(nrow);
for (row = 0; row < nrow; row++) {
// if the row doesn't exist, create it
if (!res[row])
res[row] = new Array(ncol);
if (ncol > 0)
res[row][0] = arr[row][0];
for (col = 1; col < ncol; col++)
res[row][col] = func(res[row][col-1], arr[row][col]);
}
return res.length === 1 ? res[0] : res;
};
// Destructively alter an array.
jStat.alter = function alter(arr, func) {
return jStat.map(arr, func, true);
};
// Generate a rows x cols matrix according to the supplied function.
jStat.create = function create(rows, cols, func) {
var res = new Array(rows);
var i, j;
if (isFunction(cols)) {
func = cols;
cols = rows;
}
for (i = 0; i < rows; i++) {
res[i] = new Array(cols);
for (j = 0; j < cols; j++)
res[i][j] = func(i, j);
}
return res;
};
function retZero() { return 0; }
// Generate a rows x cols matrix of zeros.
jStat.zeros = function zeros(rows, cols) {
if (!isNumber(cols))
cols = rows;
return jStat.create(rows, cols, retZero);
};
function retOne() { return 1; }
// Generate a rows x cols matrix of ones.
jStat.ones = function ones(rows, cols) {
if (!isNumber(cols))
cols = rows;
return jStat.create(rows, cols, retOne);
};
// Generate a rows x cols matrix of uniformly random numbers.
jStat.rand = function rand(rows, cols) {
if (!isNumber(cols))
cols = rows;
return jStat.create(rows, cols, jStat._random_fn);
};
function retIdent(i, j) { return i === j ? 1 : 0; }
// Generate an identity matrix of size row x cols.
jStat.identity = function identity(rows, cols) {
if (!isNumber(cols))
cols = rows;
return jStat.create(rows, cols, retIdent);
};
// Tests whether a matrix is symmetric
jStat.symmetric = function symmetric(arr) {
var size = arr.length;
var row, col;
if (arr.length !== arr[0].length)
return false;
for (row = 0; row < size; row++) {
for (col = 0; col < size; col++)
if (arr[col][row] !== arr[row][col])
return false;
}
return true;
};
// Set all values to zero.
jStat.clear = function clear(arr) {
return jStat.alter(arr, retZero);
};
// Generate sequence.
jStat.seq = function seq(min, max, length, func) {
if (!isFunction(func))
func = false;
var arr = [];
var hival = calcRdx(min, max);
var step = (max * hival - min * hival) / ((length - 1) * hival);
var current = min;
var cnt;
// Current is assigned using a technique to compensate for IEEE error.
// TODO: Needs better implementation.
for (cnt = 0;
current <= max && cnt < length;
cnt++, current = (min * hival + step * hival * cnt) / hival) {
arr.push((func ? func(current, cnt) : current));
}
return arr;
};
// arange(5) -> [0,1,2,3,4]
// arange(1,5) -> [1,2,3,4]
// arange(5,1,-1) -> [5,4,3,2]
jStat.arange = function arange(start, end, step) {
var rl = [];
var i;
step = step || 1;
if (end === undefined) {
end = start;
start = 0;
}
if (start === end || step === 0) {
return [];
}
if (start < end && step < 0) {
return [];
}
if (start > end && step > 0) {
return [];
}
if (step > 0) {
for (i = start; i < end; i += step) {
rl.push(i);
}
} else {
for (i = start; i > end; i += step) {
rl.push(i);
}
}
return rl;
};
// A=[[1,2,3],[4,5,6],[7,8,9]]
// slice(A,{row:{end:2},col:{start:1}}) -> [[2,3],[5,6]]
// slice(A,1,{start:1}) -> [5,6]
// as numpy code A[:2,1:]
jStat.slice = (function(){
function _slice(list, start, end, step) {
// note it's not equal to range.map mode it's a bug
var i;
var rl = [];
var length = list.length;
if (start === undefined && end === undefined && step === undefined) {
return jStat.copy(list);
}
start = start || 0;
end = end || list.length;
start = start >= 0 ? start : length + start;
end = end >= 0 ? end : length + end;
step = step || 1;
if (start === end || step === 0) {
return [];
}
if (start < end && step < 0) {
return [];
}
if (start > end && step > 0) {
return [];
}
if (step > 0) {
for (i = start; i < end; i += step) {
rl.push(list[i]);
}
} else {
for (i = start; i > end;i += step) {
rl.push(list[i]);
}
}
return rl;
}
function slice(list, rcSlice) {
var colSlice, rowSlice;
rcSlice = rcSlice || {};
if (isNumber(rcSlice.row)) {
if (isNumber(rcSlice.col))
return list[rcSlice.row][rcSlice.col];
var row = jStat.rowa(list, rcSlice.row);
colSlice = rcSlice.col || {};
return _slice(row, colSlice.start, colSlice.end, colSlice.step);
}
if (isNumber(rcSlice.col)) {
var col = jStat.cola(list, rcSlice.col);
rowSlice = rcSlice.row || {};
return _slice(col, rowSlice.start, rowSlice.end, rowSlice.step);
}
rowSlice = rcSlice.row || {};
colSlice = rcSlice.col || {};
var rows = _slice(list, rowSlice.start, rowSlice.end, rowSlice.step);
return rows.map(function(row) {
return _slice(row, colSlice.start, colSlice.end, colSlice.step);
});
}
return slice;
}());
// A=[[1,2,3],[4,5,6],[7,8,9]]
// sliceAssign(A,{row:{start:1},col:{start:1}},[[0,0],[0,0]])
// A=[[1,2,3],[4,0,0],[7,0,0]]
jStat.sliceAssign = function sliceAssign(A, rcSlice, B) {
var nl, ml;
if (isNumber(rcSlice.row)) {
if (isNumber(rcSlice.col))
return A[rcSlice.row][rcSlice.col] = B;
rcSlice.col = rcSlice.col || {};
rcSlice.col.start = rcSlice.col.start || 0;
rcSlice.col.end = rcSlice.col.end || A[0].length;
rcSlice.col.step = rcSlice.col.step || 1;
nl = jStat.arange(rcSlice.col.start,
Math.min(A.length, rcSlice.col.end),
rcSlice.col.step);
var m = rcSlice.row;
nl.forEach(function(n, i) {
A[m][n] = B[i];
});
return A;
}
if (isNumber(rcSlice.col)) {
rcSlice.row = rcSlice.row || {};
rcSlice.row.start = rcSlice.row.start || 0;
rcSlice.row.end = rcSlice.row.end || A.length;
rcSlice.row.step = rcSlice.row.step || 1;
ml = jStat.arange(rcSlice.row.start,
Math.min(A[0].length, rcSlice.row.end),
rcSlice.row.step);
var n = rcSlice.col;
ml.forEach(function(m, j) {
A[m][n] = B[j];
});
return A;
}
if (B[0].length === undefined) {
B = [B];
}
rcSlice.row.start = rcSlice.row.start || 0;
rcSlice.row.end = rcSlice.row.end || A.length;
rcSlice.row.step = rcSlice.row.step || 1;
rcSlice.col.start = rcSlice.col.start || 0;
rcSlice.col.end = rcSlice.col.end || A[0].length;
rcSlice.col.step = rcSlice.col.step || 1;
ml = jStat.arange(rcSlice.row.start,
Math.min(A.length, rcSlice.row.end),
rcSlice.row.step);
nl = jStat.arange(rcSlice.col.start,
Math.min(A[0].length, rcSlice.col.end),
rcSlice.col.step);
ml.forEach(function(m, i) {
nl.forEach(function(n, j) {
A[m][n] = B[i][j];
});
});
return A;
};
// [1,2,3] ->
// [[1,0,0],[0,2,0],[0,0,3]]
jStat.diagonal = function diagonal(diagArray) {
var mat = jStat.zeros(diagArray.length, diagArray.length);
diagArray.forEach(function(t, i) {
mat[i][i] = t;
});
return mat;
};
// return copy of A
jStat.copy = function copy(A) {
return A.map(function(row) {
if (isNumber(row))
return row;
return row.map(function(t) {
return t;
});
});
};
// TODO: Go over this entire implementation. Seems a tragic waste of resources
// doing all this work. Instead, and while ugly, use new Function() to generate
// a custom function for each static method.
// Quick reference.
var jProto = jStat.prototype;
// Default length.
jProto.length = 0;
// For internal use only.
// TODO: Check if they're actually used, and if they are then rename them
// to _*
jProto.push = Array.prototype.push;
jProto.sort = Array.prototype.sort;
jProto.splice = Array.prototype.splice;
jProto.slice = Array.prototype.slice;
// Return a clean array.
jProto.toArray = function toArray() {
return this.length > 1 ? slice.call(this) : slice.call(this)[0];
};
// Map a function to a matrix or vector.
jProto.map = function map(func, toAlter) {
return jStat(jStat.map(this, func, toAlter));
};
// Cumulatively combine the elements of a matrix or vector using a function.
jProto.cumreduce = function cumreduce(func, toAlter) {
return jStat(jStat.cumreduce(this, func, toAlter));
};
// Destructively alter an array.
jProto.alter = function alter(func) {
jStat.alter(this, func);
return this;
};
// Extend prototype with methods that have no argument.
(function(funcs) {
for (var i = 0; i < funcs.length; i++) (function(passfunc) {
jProto[passfunc] = function(func) {
var self = this,
results;
// Check for callback.
if (func) {
setTimeout(function() {
func.call(self, jProto[passfunc].call(self));
});
return this;
}
results = jStat[passfunc](this);
return isArray(results) ? jStat(results) : results;
};
})(funcs[i]);
})('transpose clear symmetric rows cols dimensions diag antidiag'.split(' '));
// Extend prototype with methods that have one argument.
(function(funcs) {
for (var i = 0; i < funcs.length; i++) (function(passfunc) {
jProto[passfunc] = function(index, func) {
var self = this;
// check for callback
if (func) {
setTimeout(function() {
func.call(self, jProto[passfunc].call(self, index));
});
return this;
}
return jStat(jStat[passfunc](this, index));
};
})(funcs[i]);
})('row col'.split(' '));
// Extend prototype with simple shortcut methods.
(function(funcs) {
for (var i = 0; i < funcs.length; i++) (function(passfunc) {
jProto[passfunc] = function() {
return jStat(jStat[passfunc].apply(null, arguments));
};
})(funcs[i]);
})('create zeros ones rand identity'.split(' '));
// Exposing jStat.
return jStat;
}(Math));