├── .editorconfig ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── index.js ├── lib └── index.js ├── package.json └── test └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | # Directories # 2 | ############### 3 | reports/ 4 | 5 | # Node.js # 6 | ########### 7 | /node_modules/ 8 | 9 | # Git # 10 | ####### 11 | .git* 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "camelcase": false, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "es3": false, 7 | "forin": true, 8 | "freeze": true, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": "nofunc", 12 | "newcap": true, 13 | "noarg": true, 14 | "noempty": false, 15 | "nonbsp": true, 16 | "nonew": true, 17 | "plusplus": false, 18 | "undef": true, 19 | "unused": true, 20 | "strict": true, 21 | "maxparams": 10, 22 | "maxdepth": 5, 23 | "maxstatements": 100, 24 | "maxcomplexity": false, 25 | "maxlen": 1000, 26 | "asi": false, 27 | "boss": false, 28 | "debug": false, 29 | "eqnull": false, 30 | "esnext": true, 31 | "evil": false, 32 | "expr": true, 33 | "funcscope": false, 34 | "globalstrict": false, 35 | "iterator": false, 36 | "lastsemic": false, 37 | "laxbreak": false, 38 | "laxcomma": false, 39 | "loopfunc": false, 40 | "maxerr": 1000, 41 | "moz": false, 42 | "multistr": false, 43 | "notypeof": false, 44 | "proto": false, 45 | "scripturl": false, 46 | "shadow": false, 47 | "sub": true, 48 | "supernew": false, 49 | "validthis": false, 50 | "noyield": false, 51 | "browser": true, 52 | "browserify": true, 53 | "couch": false, 54 | "devel": true, 55 | "dojo": false, 56 | "jasmine": false, 57 | "jquery": false, 58 | "mocha": true, 59 | "mootools": false, 60 | "node": true, 61 | "nonstandard": false, 62 | "prototypejs": false, 63 | "qunit": false, 64 | "quotmark": "single", 65 | "rhino": false, 66 | "shelljs": false, 67 | "worker": false, 68 | "wsh": false, 69 | "yui": false, 70 | "globals": {} 71 | } 72 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git* 3 | 4 | # Utilities # 5 | ############# 6 | .jshintrc 7 | .jshintignore 8 | .travis.yml 9 | .editorconfig 10 | 11 | # Directories # 12 | ############### 13 | reports/ 14 | test/ 15 | 16 | # Node.js # 17 | ########### 18 | .npmignore 19 | /node_modules/ 20 | 21 | # Logs # 22 | ######## 23 | *.log 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | - "0.10" 6 | - "iojs" 7 | after_script: 8 | - npm run coveralls 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Philipp Burckhardt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM version][npm-image]][npm-url] 2 | [![Build Status][travis-image]][travis-url] 3 | [![Coverage Status][coveralls-image]][coveralls-url] 4 | [![Dependencies][dependencies-image]][dependencies-url] 5 | 6 | # kernelSmooth 7 | 8 | > nonparametric kernel smoothing for JavaScript 9 | 10 | ## Installation 11 | 12 | Via npm: 13 | ``` 14 | npm install kernel-smooth 15 | ``` 16 | 17 | Require as follows: 18 | ``` 19 | var kernel = require('kernel-smooth'); 20 | ``` 21 | 22 | ## API 23 | 24 | ### .density(xs, kernel, [bandwidth]) 25 | 26 | Given input data `xs`, a kernel function and a bandwidth (if not supplied, 27 | a default value of 0.5 is used), this function returns a basic kernel density 28 | estimator: a function of one variable, `x`, which when invoked returns the 29 | kernel density estimate for `x`. The returned function can also be called with a 30 | vector supplied as an argument for `x`. In this case, the density is evaluated 31 | is for each element of the vector and the vector of density estimates 32 | is returned. 33 | 34 | ### .regression(xs, ys, kernel, [bandwidth]) 35 | 36 | Given input predictors `xs` and observed responses `ys`, a kernel function 37 | and a bandwidth (if not supplied, a default value of 0.5 is used), 38 | this function returns the Nadaraya & Watson kernel regression estimator: 39 | a function of one variable, `x`, which when invoked returns the 40 | estimate for `y`. The returned function can also be called with a 41 | vector supplied as an argument for `x`. In this case, predictions are generated 42 | for each element of the vector and the vector of predictions 43 | is returned. 44 | 45 | ### .mutipleRegression(Xs, ys, kernel, [bandwidth]) 46 | 47 | Similar to .regression(), except that Xs should be a 2d array containing multiple predictors. Each element of `Xs` should has to be an array of length `p`, with `p` denoting the number of predictors. The returned estimator generates a prediction for a new data point x = (x_1, ..., x_p). If a 2d array is supplied instead, predictions are generated for multiple data points at once, where each row (= element of the outer array) is assumed to be a datum x = (x_1, ..., x_p). 48 | 49 | ### Choice of Kernel function 50 | 51 | For the `kernel` parameter in above functions, you should supply a univariate function `K(x)` which satisfies K(x) >= 0, integrates to one, has zero mean and unit variance. 52 | See the functions in the exported `.fun` object for a list of already implemented kernel functions. 53 | 54 | ### .fun 55 | This object of the module holds the following kernel functions to be used for 56 | kernel smoothing: 57 | 58 | #### .gaussian(x) 59 | Gaussian kernel, pdf of standard normal distribution. 60 | 61 | #### .boxcar(x) 62 | Boxcar kernel, defined as 0.5 if |x| <= 1 and 0 otherwise. 63 | 64 | #### .epanechnikov(x) 65 | Epanechnikov kernel. Equal to zero if |x| > 1. Otherwise defined as 66 | 0.75 * (1 - x^2). 67 | 68 | #### .tricube(x) 69 | Tricube kernel function. Equal to zero if |x| > 1 and otherwise equal to 70 | (70/81) * (1-|x|^3)^3. 71 | 72 | ### .silverman(x) 73 | For input vector x, calculate the optimal bandwidthe using Silverman's rule of thumb. This utility function can be used to calculate an appropriate bandwidth for the case in which a Gaussian kernel is used and one has reason to believe that the data points x_i are drawn from a normal distribution. 74 | 75 | 76 | ## License 77 | 78 | MIT © [Philipp Burckhardt](http://www.philipp-burckhardt.com) 79 | 80 | [npm-url]: https://npmjs.org/package/kernel-smooth 81 | [npm-image]: https://badge.fury.io/js/kernel-smooth.svg 82 | 83 | [travis-url]: https://travis-ci.org/Planeshifter/kernel-smooth 84 | [travis-image]: https://travis-ci.org/Planeshifter/kernel-smooth.svg?branch=master 85 | 86 | [coveralls-image]: https://img.shields.io/coveralls/Planeshifter/kernel-smooth/master.svg 87 | [coveralls-url]: https://coveralls.io/r/Planeshifter/kernel-smooth?branch=master 88 | 89 | [dependencies-image]: https://david-dm.org/Planeshifter/kernel-smooth.svg?theme=shields.io 90 | [dependencies-url]: https://david-dm.org/Planeshifter/kernel-smooth 91 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var kernel = require( './../lib/' ); 4 | 5 | var x = [-2, 0, 0, 1, 1, 2, 3, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 7, 8, 10]; 6 | var y = [-4.4938900, -0.8583945, 1.0272176, 1.5572244, 1.3661168, 4.7993054, 4.8316603, 7 | 7.7616223, 8.1379193, 8.6355344, 10.6714735, 9.8003618, 10.9428430, 9.0848237, 10.9115011, 8 | 11.0412513, 10.9206553, 14.4753870, 18.2548619, 20.4272286]; 9 | 10 | 11 | var f_hat = kernel.regression( x, y, kernel.fun.gaussian, 0.5 ); 12 | 13 | console.log( f_hat( 0 ) ); 14 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // MODULES // 4 | 5 | var phi = require( 'distributions-normal-pdf' ); 6 | var d = require( 'euclidean-distance' ); 7 | var stdev = require( 'compute-stdev' ); 8 | var isArray = require( 'validate.io-array' ); 9 | var isFunction = require( 'validate.io-function' ); 10 | 11 | 12 | // Namespace object 13 | var ns = {}; 14 | 15 | // Collection of Kernel functions 16 | ns.fun = { 17 | gaussian: function( x ) { 18 | return phi(x); 19 | }, 20 | boxcar: function( x ) { 21 | return ( Math.abs( x ) <= 1 ) ? 0.5 : 0; 22 | }, 23 | epanechnikov: function( x ) { 24 | return ( Math.abs( x ) <= 1 ) ? 0.75 * ( 1 - x*x ) : 0; 25 | }, 26 | tricube: function( x ) { 27 | function tricubed( x ) { 28 | var x_abs = 1 - Math.pow( Math.abs( x ), 3 ); 29 | return Math.pow( x_abs, 3 ); 30 | } 31 | return ( Math.abs(x) <= 1 ) ? ( 70/81 ) * tricubed( x ) : 0 ; 32 | } 33 | }; 34 | 35 | // calculates weight for i-th obs 36 | function weight( kernel, bandwidth, x_0, x_i ) { 37 | var arg = (x_i - x_0) / bandwidth; 38 | return kernel( arg ); 39 | } 40 | 41 | // calculates weight for i-th obs when p > 1 42 | function weight_vectors( kernel, bandwidth, x_0, x_i ) { 43 | var arg = d( x_i, x_0 ) / bandwidth; 44 | return kernel( arg ); 45 | } 46 | 47 | // sum elements of an array 48 | function sum( arr ) { 49 | var ret = 0; 50 | for( var i = 0; i < arr.length; i++ ){ 51 | ret += arr[ i ]; 52 | } 53 | return ret; 54 | } 55 | 56 | // allow a function to be called with a vector instead of a single number 57 | function vectorize( fun ) { 58 | return function( x ) { 59 | if( isArray( x ) === false ) { 60 | return fun( x ); 61 | } else { 62 | return x.map( function( x ) { 63 | return fun( x ); 64 | } ); 65 | } 66 | }; 67 | } 68 | 69 | // allow a function to be called with a 2d array instead of a single 70 | // p-dimensional vector 71 | function matrixize( fun ) { 72 | return function( X ) { 73 | if ( isArray( X ) === true ) { 74 | if ( isArray(X[0]) === false ) { 75 | return fun( X ); 76 | } else { 77 | return X.map( function( x_row ) { 78 | return fun( x_row ); 79 | } ); 80 | } 81 | } else { 82 | throw new TypeError( 'Parameter expects array' ); 83 | } 84 | }; 85 | } 86 | 87 | 88 | // calculate optimal bandwidth according to Silverman's rule of thumb for 89 | // kernel density estimation under Gaussian data 90 | ns.silverman = function( x ) { 91 | var num = 4 * Math.pow( stdev( x ), 5 ); 92 | var denom = 3 * x.length; 93 | var divisionResult = num / denom; 94 | return Math.pow( divisionResult, 0.2 ); 95 | }; 96 | 97 | // kernel density estimation 98 | ns.density = function( xs, kernel, bandwidth ) { 99 | if ( bandwidth <= 0 ) { 100 | throw new RangeError( 'Bandwidth has to be a positive number.' ); 101 | } 102 | if ( isFunction( kernel ) === false ) { 103 | throw new TypeError( 'Kernel function has to be supplied.' ); 104 | } 105 | bandwidth = bandwidth || 0.5; 106 | var _xs = xs; 107 | var n = xs.length; 108 | var weight_fun = weight.bind( null, kernel, bandwidth ); 109 | 110 | var kernel_smoother = function( x ) { 111 | var weights = _xs.map( function( x_i ) { 112 | return weight_fun( x, x_i ); 113 | } ); 114 | var num = sum( weights ); 115 | var denom = n * bandwidth; 116 | return num / denom; 117 | }; 118 | 119 | return vectorize( kernel_smoother ); 120 | }; 121 | 122 | // kernel regression smoothing, returns function which can be evaluated at 123 | // different values of x 124 | ns.regression = function( xs, ys, kernel, bandwidth ){ 125 | bandwidth = bandwidth || 0.5; 126 | if ( bandwidth < 0 ) { 127 | throw new RangeError( 'Bandwidth has to be a positive number.' ); 128 | } 129 | if ( !ys ) { 130 | throw new TypeError( 'Numeric y must be supplied. For density estimation' + 131 | 'use .density() function' ); 132 | } 133 | if( isFunction(kernel) === false ) { 134 | throw new TypeError( 'Kernel function has to be supplied.' ); 135 | } 136 | 137 | var _xs = xs; 138 | var _ys = ys; 139 | var weight_fun = weight.bind( null, kernel, bandwidth ); 140 | 141 | var kernel_smoother = function( x ) { 142 | var weights = _xs.map( function( x_i ) { 143 | return weight_fun( x, x_i ); 144 | }); 145 | var num = sum( weights.map( function( w, i ) { 146 | return w * _ys[i]; 147 | } ) ); 148 | var denom = sum( weights ); 149 | return num / denom; 150 | }; 151 | return vectorize( kernel_smoother ); 152 | }; 153 | 154 | // similar to .regression(), but for the case of multiple predictors. 155 | ns.multipleRegression = function( Xs, ys, kernel, bandwidth ) { 156 | if ( bandwidth <= 0 ) { 157 | throw new RangeError( 'Bandwidth has to be a positive number.' ); 158 | } 159 | if ( !ys ) { 160 | throw new TypeError('Numeric y must be supplied. For density estimation' + 161 | 'use .density() function' ); 162 | } 163 | if ( isFunction(kernel) === false ) { 164 | throw new TypeError( 'Kernel function has to be supplied.' ); 165 | } 166 | if ( isArray(Xs) === false || isArray(Xs[0]) === false ) { 167 | throw new TypeError( 'Xs has to be a two-dimensional array' ); 168 | } 169 | 170 | bandwidth = bandwidth || 0.5; 171 | var _Xs = Xs; 172 | var _ys = ys; 173 | var _p = Xs[0].length; 174 | var weight_fun = weight_vectors.bind( null, kernel, bandwidth ); 175 | 176 | var kernel_smoother = function( x ) { 177 | if( isArray(x) === false || x.length !== _p ) { 178 | throw new TypeError( 'Argument has to be array of length ' + _p ); 179 | } 180 | 181 | var weights = _Xs.map( function( x_i ) { 182 | return weight_fun( x, x_i ); 183 | } ); 184 | var num = sum( weights.map( function( w, i ) { 185 | return w * _ys[i]; 186 | } ) ); 187 | var denom = sum( weights ); 188 | return num / denom; 189 | }; 190 | 191 | return matrixize( kernel_smoother ); 192 | }; 193 | 194 | 195 | // EXPORTS // 196 | 197 | module.exports = exports = ns; 198 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kernel-smooth", 3 | "version": "0.2.3", 4 | "description": "nonparametric kernel smoothing", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha", 8 | "test-cov": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --dir ./reports/coverage -- -R spec", 9 | "coveralls": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha --dir ./reports/coveralls/coverage --report lcovonly -- -R spec && cat ./reports/coveralls/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./reports/coveralls" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/Planeshifter/kernelSmoother.js.git" 14 | }, 15 | "keywords": [ 16 | "statistics", 17 | "nonparametrics", 18 | "kernel", 19 | "smoothing" 20 | ], 21 | "author": "Philipp Burckhardt", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/Planeshifter/kernelSmoother.js/issues" 25 | }, 26 | "homepage": "https://github.com/Planeshifter/kernelSmoother.js", 27 | "dependencies": { 28 | "compute-stdev": "^1.0.0", 29 | "distributions-normal-pdf": "0.0.2", 30 | "euclidean-distance": "^0.1.0", 31 | "validate.io-array": "^1.0.6", 32 | "validate.io-function": "^1.0.2" 33 | }, 34 | "devDependencies": { 35 | "chai": "^2.0.0", 36 | "coveralls": "^2.11.2", 37 | "istanbul": "^0.3.13", 38 | "jshint": "^2.7.0", 39 | "jshint-stylish": "^1.0.1", 40 | "mocha": "^2.1.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var chai = require( 'chai' ); 2 | var expect = chai.expect; 3 | var kernel = require( './../lib/' ); 4 | 5 | describe(".fun", function(){ 6 | it("is an object of kernel functions", function(){ 7 | expect(kernel.fun).to.be.an("object"); 8 | expect(kernel.fun).to.include.keys(["gaussian", "boxcar", "epanechnikov", "tricube"]); 9 | }); 10 | }); 11 | 12 | describe(".density()", function(){ 13 | it("throws when negative bandwidth is provided", function(){ 14 | expect(function(){ 15 | kernel.density([-2, 0, 1], kernel.fun.gaussian, -2); 16 | }).to.throw(Error); 17 | }); 18 | 19 | it("throws when no kernel function is provided", function(){ 20 | expect(function(){ 21 | kernel.density([-2, 0, 1], null, 0.3); 22 | }).to.throw(Error); 23 | }); 24 | 25 | it("returns a function", function(){ 26 | var f = kernel.density([-2, 0, 1], kernel.fun.gaussian, 0.3); 27 | expect(f).to.be.a("function"); 28 | }); 29 | }); 30 | 31 | describe(".regression()", function(){ 32 | it("throws when negative bandwidth is provided", function(){ 33 | expect(function(){ 34 | kernel.regression([-2, 0, 1], [2,3,5], kernel.fun.gaussian, -2); 35 | }).to.throw(Error); 36 | }); 37 | 38 | it("throws when no kernel function is provided", function(){ 39 | expect(function(){ 40 | kernel.regression([-2, 0, 1], [2,3,5], null, 0.3); 41 | }).to.throw(Error); 42 | }); 43 | 44 | it("throws when no ys are supplied", function(){ 45 | expect(function(){ 46 | kernel.regression([-2, 0, 1], null, kernel.fun.gaussian, 0.3); 47 | }).to.throw(Error); 48 | }); 49 | 50 | it("returns a function", function(){ 51 | var f = kernel.regression([-2, 0, 1], [2,3,5], kernel.fun.gaussian, 0.3); 52 | expect(f).to.be.a("function"); 53 | }); 54 | }); 55 | 56 | describe(".multipleRegression()", function(){ 57 | it("throws when negative bandwidth is provided", function(){ 58 | expect(function(){ 59 | kernel.multipleRegression([[-2, 0],[3, 5],[8, 8]], [2,3,5], kernel.fun.gaussian, -2); 60 | }).to.throw(Error); 61 | }); 62 | 63 | it("throws when no kernel function is provided", function(){ 64 | expect(function(){ 65 | kernel.multipleRegression([[-2, 0],[3, 5],[8, 8]], [2,3,5], null, 0.3); 66 | }).to.throw(Error); 67 | }); 68 | 69 | it("throws when no ys are supplied", function(){ 70 | expect(function(){ 71 | kernel.multipleRegression([[-2, 0],[3, 5],[8, 8]], null, kernel.fun.gaussian, 0.3); 72 | }).to.throw(Error); 73 | }); 74 | 75 | it("returns a function", function(){ 76 | var f = kernel.multipleRegression([[-2, 0],[3, 5],[8, 8]], [2,3,5], kernel.fun.gaussian, 0.3); 77 | expect(f).to.be.a("function"); 78 | }); 79 | it("the returned function expects array of length p as input", function(){ 80 | var f = kernel.multipleRegression([[2, 0],[3, 5],[8, 8]], [2,3,5], kernel.fun.gaussian, 0.3); 81 | var x_p = [1,3]; 82 | var res = f(x_p); 83 | expect(res).to.be.a("number"); 84 | expect(function(){ 85 | f([2,3,4]); 86 | }).to.throw(Error); 87 | }); 88 | }); 89 | 90 | describe(".silverman()", function(){ 91 | it("returns an optimal bandwith", function(){ 92 | var x = [0,0,1,1,1,2,2]; 93 | var h = kernel.silverman(x); 94 | expect(h).to.be.closeTo(0.586, 0.01); 95 | }); 96 | }); 97 | --------------------------------------------------------------------------------