Skip to content

Instantly share code, notes, and snippets.

@rpgove
Last active January 27, 2018 18:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rpgove/073d6cb996d7de1d52935790139c4240 to your computer and use it in GitHub Desktop.
Save rpgove/073d6cb996d7de1d52935790139c4240 to your computer and use it in GitHub Desktop.
Kernel smoothing
license: gpl-3.0
height: 300
scrolling: no
border: yes

Kernel smoothing is a method to estimate a smooth line from several discrete points, which is in contrast with kernel density estimation which is used to estimate a probability distribution from discrete points. This example uses the Nadaraya-Watson kernel-weighted average with a Gaussian kernel on random data that follows a noisey sine wave.

The smoothness is controlled by a bandwidth parameter. If the bandwidth is too small, the line essentially connects the existing data points. If the bandwidth is too large, then the line essentially becomes a straight line.

This method is usually fast if there are a small number of data points, but a tree structure can speed up the calculation if there is a lot of data.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
html, body {
margin: 0;
}
circle {
fill: #666;
}
path {
fill: none;
stroke: teal;
stroke-width: 2px;
opacity: 0.5;
}
</style>
<div>
Bandwidth: <span id="current-bandwidth">15</span><br>
1<input type="range" min="1" max="200" value="15" class="slider" id="bandwidth-slider">200
</div>
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var margin = {left: 10, top: 10, right: 10, bottom: 10};
var width = 960 - margin.left - margin.right;
var height = 220 - margin.top - margin.bottom;
// Generate source particles in a sine wave with random noise
var numbers = d3.range(0, width, 10).map(function (d,i) {
return [d, height * (0.4 * Math.sin(d/(0.075*width)) + 0.4 + Math.random()/5)];
});
var line = d3.line();
var svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left +',' + margin.top + ')');
// Plot a circle for each source particle
svg.append('g')
.attr('class', 'data')
.selectAll('circle').data(numbers)
.enter().append('circle')
.attr('r', 2)
.attr('cx', function (d) { return d[0]; })
.attr('cy', function (d) { return d[1]; });
svg.append('g')
.attr('class', 'smoothed')
.append('path');
d3.select('#bandwidth-slider')
.on('input', renderLine);
renderLine();
function renderLine() {
// Get the value of the slider
var bandwidth = +document.getElementById('bandwidth-slider').value;
d3.select('#current-bandwidth').text(bandwidth)
svg.select('.smoothed path')
.datum(smoothedNumbers(d3.range(0, width, 10), numbers, bandwidth))
.attr('d', line);
}
// Compute estimated value at each target x coordinate using the
// source particles (the samples).
function smoothedNumbers (targets, sources, bandwidth) {
return targets.map(function (t) {
var numerator = d3.sum(sources, function (s) {
return gaussian(s[0], t, bandwidth) * s[1];
});
var denominator = d3.sum(sources, function (s) {
return gaussian(s[0], t, bandwidth);
});
return [t, numerator / denominator];
});
}
function gaussian (target, source, bandwidth) {
return Math.exp(-Math.pow(target - source, 2) / (2*bandwidth*bandwidth));
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment