From 6f692a47e37f80dda9710353833b3fe6b39f2fd6 Mon Sep 17 00:00:00 2001 From: kgryte Date: Mon, 8 Dec 2014 22:28:26 -0800 Subject: [PATCH] [UPDATE] lib, tests, examples. --- README.md | 61 +++++++++++++++++++++++++++-- examples/index.js | 18 ++++++++- lib/index.js | 77 +++++++++++++++++++++++++++++++++---- package.json | 19 +++++++++- test/test.js | 97 +++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 255 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 78de6e0..d289d44 100644 --- a/README.md +++ b/README.md @@ -19,18 +19,71 @@ For use in the browser, use [browserify](https://github.com/substack/node-browse To use the module, ``` javascript -var foo = require( 'compute-incrmstdev' ); +var incrmstdev = require( 'compute-incrmstdev' ); ``` -#### foo( arr ) +#### incrmstdev( window ) + +Returns an initialized method to compute a moving sample standard deviation incrementally. `window` sets the window size, i.e., the number of values over which to compute a moving sample standard deviation. + +``` javascript +var mstdev = incrmstdev( 3 ); +``` + +#### mstdev( [value] ) + +If provided a `value`, the method updates and returns the sample standard deviation of the current window. If not provided a `value`, the method returns the current sample standard deviation. + +``` javascript +var sigma; + +// Filling window... +sigma = mstdev( 2 ); +// stdev is 0 + +mstdev( 4 ); +// stdev is ~1.414 + +mstdev( 0 ); +// stdev is 2 + +// Window starts sliding... +mstdev( -2 ); +// stdev is ~3.082 + +mstdev( -1 ); +// mstdev is 1 + +sigma = mstdev(); +// returns 1 +``` + + +## Notes + +1. If values have not yet been provided to `mstdev`, `mstdev` returns `null`. +1. The first `W-1` returned sample standard deviations will have less statistical support than subsequent sample standard deviations, as `W` values are needed to fill the window buffer. Until the window is full, the sample standard deviation returned equals the [sample standard deviation](https://github.com/compute-io/stdev) of all values provided thus far. + +The use case for this module differs from the conventional [vector](https://github.com/compute-io/mstdev) implementation and the [stream](https://github.com/flow-io/) implementation. Namely, this module decouples the act of updating the moving sample standard deviation from the act of consuming the moving sample standard deviation. -What does this function do? ## Examples ``` javascript -var foo = require( 'compute-incrmstdev' ); +var incrmstdev = require( 'compute-incrmstdev' ); + +// Initialize a method to calculate the moving sample standard deviation incrementally: +var mstdev = incrmstdev( 5 ), + sigma; + +// Simulate some data... +for ( var i = 0; i < 1000; i++ ) { + sigma = mstdev( Math.random()*100 ); + console.log( sigma ); +} +sigma = mstdev(); +console.log( sigma ); ``` To run the example code from the top-level application directory, diff --git a/examples/index.js b/examples/index.js index 791518a..8f1ae92 100644 --- a/examples/index.js +++ b/examples/index.js @@ -1,3 +1,19 @@ 'use strict'; -var module = require( './../lib' ); +var incrmstdev = require( './../lib' ); + +// Initialize a method to calculate the moving sample standard deviation incrementally: +var mstdev = incrmstdev( 5 ); + +// Simulate some data... +var value, sigma; + +console.log( '\nValue\tSample StDev\n' ); + +for ( var i = 0; i < 100; i++ ) { + + value = Math.random() * 100; + sigma = mstdev( value ); + + console.log( '%d\t%d', value.toFixed( 4 ), sigma.toFixed( 4 ) ); +} diff --git a/lib/index.js b/lib/index.js index ce6832a..f723c8f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -30,20 +30,83 @@ // MODULES // -// var module_alias = require( 'module_name' ); +var isInteger = require( 'validate.io-integer' ); -// FUNCTIONS // +// INCREMENTAL MOVING SAMPLE STANDARD DEVIATION // /** -* FUNCTION: foo() -* {{ foo description }}. +* FUNCTION: incrmstdev( W ) +* Returns a method to compute a moving sample standard deviation incrementally. +* +* @param {Number} W - window size +* @returns {Function} method to compute a moving sample standard deviation incrementally */ -function foo() { +function incrmstdev( W ) { + if ( !isInteger( W ) || W < 1 ) { + throw new TypeError( 'incrmstdev()::invalid input argument. Window size must be a positive integer.' ); + } + var arr = new Array( W ), + n = W - 1, + M2 = 0, + mu = 0, + N = 0, + i = -1, + delta, + tmp, + d1, + d2; + /** + * FUNCTION: incrmstdev( [value] ) + * If a `value` is provided, updates and returns the updated sample standard deviation. If no `value` is provided, returns the current sample standard deviation. + * + * @param {Number} [value] - value used to update the moving sample standard deviation + * @returns {Number} sample standard deviation + */ + return function incrmstdev( x ) { + if ( !arguments.length ) { + if ( N === 0 ) { + return null; + } + if ( N === 1 ) { + return 0; + } + if ( N < W ) { + return Math.sqrt( M2 / (N-1) ); + } + return Math.sqrt( M2 / n ); + } + // Update the index for managing the circular buffer... + i = (i+1) % W; + + // Fill up the initial window; else, update the existing window... + if ( N < W ) { + arr[ i ] = x; + N += 1; + delta = x - mu; + mu += delta / N; + M2 += delta * (x - mu); + if ( N === 1 ) { + return 0; + } + return Math.sqrt( M2 / (N-1) ); + } + if ( N === 1 ) { + return 0; + } + tmp = arr[ i ]; + arr[ i ] = x; + delta = x - tmp; + d1 = tmp - mu; + mu += delta / W; + d2 = x - mu; + M2 += delta * (d1 + d2); -} // end FUNCTION foo() + return Math.sqrt( M2 / n ); + }; +} // end FUNCTION incrmstdev() // EXPORTS // -module.exports = foo; +module.exports = incrmstdev; diff --git a/package.json b/package.json index f0c0d25..6a6800d 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,27 @@ "keywords": [ "compute.io", "compute", - "computation" + "computation", + "statistics", + "stats", + "mathematics", + "math", + "sample variance", + "variance", + "dispersion", + "moving variance", + "sample standard deviation", + "standard deviation", + "stdev", + "sliding", + "window" ], "bugs": { "url": "https://github.com/compute-io/incrmstdev/issues" }, - "dependencies": {}, + "dependencies": { + "validate.io-integer": "^1.0.2" + }, "devDependencies": { "chai": "1.x.x", "mocha": "1.x.x", diff --git a/test/test.js b/test/test.js index 491a951..6f51be3 100644 --- a/test/test.js +++ b/test/test.js @@ -1,3 +1,4 @@ +/* global describe, it, require */ 'use strict'; // MODULES // @@ -6,7 +7,7 @@ var // Expectation library: chai = require( 'chai' ), // Module to be tested: - lib = require( './../lib' ); + incrmstdev = require( './../lib' ); // VARIABLES // @@ -20,9 +21,99 @@ var expect = chai.expect, describe( 'compute-incrmstdev', function tests() { it( 'should export a function', function test() { - expect( lib ).to.be.a( 'function' ); + expect( incrmstdev ).to.be.a( 'function' ); }); - it( 'should do something' ); + it( 'should throw an error if not provided an integer', function test() { + var values = [ + '5', + -5, + 0, + true, + null, + undefined, + NaN, + [], + {}, + function(){} + ]; + + for ( var i = 0; i < values.length; i++ ) { + expect( badValue( values[i] ) ).to.throw( TypeError ); + } + + function badValue( value ) { + return function() { + incrmstdev( value ); + }; + } + }); + + it( 'should return a function', function test() { + expect( incrmstdev( 3 ) ).to.be.a( 'function' ); + }); + + it( 'should compute a moving sample standard deviation incrementally', function test() { + var data, + N, + d, + expected, + actual, + mstdev; + + data = [ 2, 3, 4, -1, 3, 1 ]; + N = data.length; + + mstdev = incrmstdev( 3 ); + + actual = new Array( N ); + for ( var i = 0; i < N; i++ ) { + d = data[ i ]; + actual[ i ] = mstdev( d ); + } + + expected = [ 0, Math.sqrt( 0.5 ), 1, Math.sqrt(7), Math.sqrt(7), 2 ]; + + assert.deepEqual( actual, expected ); + }); + + it( 'should return the current moving sample standard deviation if provided no arguments', function test() { + var data = [ 2, 3, 10 ], + len = data.length, + mstdev = incrmstdev( 3 ), + i; + + for ( i = 0; i < len-1; i++ ) { + mstdev( data[ i ] ); + } + assert.strictEqual( mstdev(), Math.sqrt( 0.5 ) ); + + for ( i = 0; i < len; i++ ) { + mstdev( data[ i ] ); + } + assert.closeTo( mstdev(), Math.sqrt(19), 1e-10 ); + }); + + it( 'should return null if asked for a moving sample standard deviation when not having received any data', function test() { + var mstdev = incrmstdev( 3 ); + assert.isNull( mstdev() ); + }); + + it( 'should return 0 if asked for a moving sample standard deviation when having received only a single datum', function test() { + var mstdev = incrmstdev( 3 ); + mstdev( 4 ); + assert.strictEqual( mstdev(), 0 ); + }); + + it( 'should always return 0 if provided a window size equal to 1', function test() { + var mstdev = incrmstdev( 1 ); + mstdev( 4 ); + assert.strictEqual( mstdev(), 0 ); + assert.strictEqual( mstdev( 5 ), 0 ); + assert.strictEqual( mstdev( 2 ), 0 ); + for ( var i = 0; i < 100; i++ ) { + assert.strictEqual( mstdev( i ), 0 ); + } + }); });