viewof labControls = {
const api = kdeApi;
const seq = api.sequence(api.DEFAULTS.seed);
const drawn = api.samplePrefix(seq, api.DEFAULTS.population, api.DEFAULTS.n);
if (drawn.error) throw new Error(drawn.error);
const ruled = api.silvermanNrd0(drawn.values);
if (ruled.error) throw new Error(ruled.error);
const start = ruled.value;
const series = [
["bias", "Bias²"],
["variance", "Variance"],
["mise", "MISE"],
["ise", "ISE"],
];
const form = labChrome.controlRow("Kernel density controls");
form.innerHTML = `<label class="control-field control-field-wide">
<span id="kde-bandwidth-label">Bandwidth h</span>
<span class="control-slider">
<input name="hslider" type="range" min="0.02" max="2.5" step="0.001" aria-labelledby="kde-bandwidth-label" aria-describedby="kde-control-hint">
<input name="hnumber" type="number" min="0.005" max="20" step="0.001" aria-label="Bandwidth h as a number">
</span>
</label>
<label class="control-field">
<span id="kde-n-label">Sample size n</span>
<span class="control-slider">
<input name="nslider" type="range" min="2" max="${api.MAX_N}" step="1" aria-labelledby="kde-n-label" aria-describedby="kde-control-hint">
<output name="nout"></output>
</span>
</label>
<label class="control-field">
<span id="kde-pop-label">Distribution</span>
<select name="population" aria-labelledby="kde-pop-label"></select>
</label>
<label class="control-field control-field-fixed">
<span id="kde-seed-label">Seed</span>
<input name="seed" type="number" step="1" inputmode="numeric" autocomplete="off" aria-labelledby="kde-seed-label">
</label>
<div class="control-actions">
<button type="button" name="newer">New sample</button>
<button type="button" name="silver">Silverman h</button>
<button type="button" name="restore">Reset</button>
</div>
<div class="control-toggles" role="group" aria-label="Panel B series"></div>
<p class="control-hint" id="kde-control-hint"></p>
<p class="control-note"></p>`;
form.querySelector("#kde-control-hint").textContent =
`n is a prefix of ${api.MAX_N} draws from this seed. Changing h does not redraw them. Arrow keys move a focused slider. Shift changes h by 0.1 and n by 10.`;
const showPopulation = () => {
const item = api.POPULATIONS.find((row) => row.id === form.population.value);
form.population.title = item ? item.label + ": " + item.detail : "";
};
for (const item of api.POPULATIONS) {
const option = document.createElement("option");
option.value = item.id;
option.textContent = item.label;
option.title = item.detail;
form.population.appendChild(option);
}
for (const [key, label] of series) {
const box = document.createElement("input");
box.type = "checkbox";
box.name = key;
box.checked = true;
const item = document.createElement("label");
item.append(box, document.createTextNode(" " + label));
form.querySelector(".control-toggles").appendChild(item);
}
const note = form.querySelector(".control-note");
const readSeries = () => series.filter(([key]) => form[key].checked).map(([key]) => key);
const snapshot = (bandwidth) => ({
bandwidth,
drawCount: Number(form.nout.value),
populationId: form.population.value,
seedValue: Number(form.seed.value),
seriesOn: readSeries(),
});
const showBandwidth = (value) => {
form.hslider.min = String(Math.min(0.02, value));
form.hslider.max = String(Math.max(2.5, value));
form.hslider.value = String(value);
form.hnumber.value = Number(value).toFixed(4);
};
const showCount = (value) => {
const n = Math.max(2, Math.min(api.MAX_N, Math.round(value)));
form.nslider.value = String(n);
form.nout.value = String(n);
form.nout.textContent = String(n);
return n;
};
showBandwidth(start);
showCount(api.DEFAULTS.n);
form.population.value = api.DEFAULTS.population;
showPopulation();
form.seed.value = String(api.DEFAULTS.seed);
form.value = {
bandwidth: start,
drawCount: api.DEFAULTS.n,
populationId: api.DEFAULTS.population,
seedValue: api.DEFAULTS.seed,
seriesOn: readSeries(),
};
const emit = (bandwidth) => {
note.textContent = "";
form.value = snapshot(bandwidth);
form.dispatchEvent(new Event("input", { bubbles: true }));
};
form.hslider.addEventListener("input", () => {
const value = Number(form.hslider.value);
showBandwidth(value);
form.value = snapshot(value);
});
form.hslider.addEventListener("keydown", (event) => {
if (!event.shiftKey) return;
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight" && event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
event.preventDefault();
const dir = event.key === "ArrowLeft" || event.key === "ArrowDown" ? -1 : 1;
const next = Math.round((Number(form.value.bandwidth) + dir * 0.1) * 1000) / 1000;
if (!(next > 0) || next > 20) return;
showBandwidth(next);
emit(next);
});
form.hnumber.addEventListener("change", () => {
const value = Number(form.hnumber.value);
if (!(value > 0) || value > 20 || !Number.isFinite(value)) {
note.textContent = "Bandwidth must be a positive finite number no larger than 20. The previous value is still in use.";
form.hnumber.value = Number(form.value.bandwidth).toFixed(4);
return;
}
showBandwidth(value);
emit(value);
});
form.nslider.addEventListener("input", () => {
showCount(Number(form.nslider.value));
form.value = snapshot(form.value.bandwidth);
});
form.nslider.addEventListener("keydown", (event) => {
if (!event.shiftKey) return;
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight" && event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
event.preventDefault();
const dir = event.key === "ArrowLeft" || event.key === "ArrowDown" ? -1 : 1;
showCount(form.value.drawCount + dir * 10);
emit(form.value.bandwidth);
});
form.population.addEventListener("change", () => {
showPopulation();
emit(form.value.bandwidth);
});
form.seed.addEventListener("change", () => {
if (form.seed.value.trim() === "") {
note.textContent = "The seed is empty. The sample was not redrawn.";
form.seed.value = String(form.value.seedValue);
return;
}
const parsed = Number(form.seed.value);
if (!Number.isInteger(parsed)) {
note.textContent = "The seed must be an integer. The sample was not redrawn.";
form.seed.value = String(form.value.seedValue);
return;
}
const wrapped = parsed >>> 0;
note.textContent = parsed === wrapped ? "" : "Stored as an unsigned 32-bit integer, so " + parsed + " uses the same sequence as " + wrapped + ".";
form.seed.value = String(wrapped);
form.value = { ...snapshot(form.value.bandwidth), seedValue: wrapped };
form.dispatchEvent(new Event("input", { bubbles: true }));
});
form.querySelector(".control-toggles").addEventListener("change", () => emit(form.value.bandwidth));
form.newer.addEventListener("click", () => {
const next = (Number(form.value.seedValue) + 1) >>> 0;
form.seed.value = String(next);
note.textContent = "";
form.value = { ...snapshot(form.value.bandwidth), seedValue: next };
form.dispatchEvent(new Event("input", { bubbles: true }));
});
form.silver.addEventListener("click", () => {
const sample = api.samplePrefix(api.sequence(form.value.seedValue), form.value.populationId, form.value.drawCount);
if (sample.error) {
note.textContent = sample.error;
return;
}
const next = api.silvermanNrd0(sample.values);
if (next.error) {
note.textContent = next.error;
return;
}
showBandwidth(next.value);
emit(next.value);
});
form.restore.addEventListener("click", (event) => {
event.preventDefault();
const again = api.samplePrefix(api.sequence(api.DEFAULTS.seed), api.DEFAULTS.population, api.DEFAULTS.n);
const next = api.silvermanNrd0(again.values);
if (again.error || next.error) {
note.textContent = (again.error || next.error);
return;
}
showBandwidth(next.value);
showCount(api.DEFAULTS.n);
form.population.value = api.DEFAULTS.population;
showPopulation();
form.seed.value = String(api.DEFAULTS.seed);
for (const [key] of series) form[key].checked = true;
note.textContent = "";
form.value = {
bandwidth: next.value,
drawCount: api.DEFAULTS.n,
populationId: api.DEFAULTS.population,
seedValue: api.DEFAULTS.seed,
seriesOn: readSeries(),
};
form.dispatchEvent(new Event("input", { bubbles: true }));
});
return form;
}Kernel density laboratory
A Gaussian kernel density estimate: move the bandwidth, keep the sample, and compare this sample’s integrated squared error with the mean integrated squared error.
A Gaussian kernel turns a sample into a density. The bandwidth is the choice this page is for. The density that produced the sample is drawn on the same axes, so the miss is visible.
What this page investigates
How the bandwidth h trades bias against variance, and how that trade shows up in two different errors. The integrated squared error is the error of the sample on the screen. The mean integrated squared error is the expected value of that error for a sample of this size from this population.
The estimate does not use the true density. Both errors do.
Predict first
The page opens on a mixture with two modes. Before moving h, predict what a very small bandwidth does to the curve, and what a large bandwidth does to those modes. Then predict whether this sample’s integrated squared error will lie on top of the mean integrated squared error.
The estimate these controls change
\widehat f_h(x)=\frac{1}{nh}\sum_{i=1}^{n}\varphi\left(\frac{x-X_i}{h}\right).
\varphi is the standard normal density. The bandwidth control is h, in the same units as x: the bump centred at X_i is the \mathrm{N}(X_i,h^2) density. That is the one-dimensional case of the estimator on the theory page, where the bandwidth matrix is H=h^2.
The sample-size control is n. It keeps the first n draws of a sequence of 200 built from the seed. Changing h does not rebuild that sequence. Changing n does not draw again. New sample advances the seed and redraws the estimate and the integrated squared error. Reset returns to seed 3500, the bimodal mixture, n=120, and the Silverman bandwidth of that sample.
At a fixed n and a fixed population, the mean integrated squared error, the integrated variance, and the integrated squared bias do not move when the sample changes. They are properties of h, n, and f.
Silverman on this page means R’s bw.nrd0,
0.9\,\min\left(s,\frac{\mathrm{IQR}}{1.34}\right)\,n^{-1/5},
with s the sample standard deviation using divisor n-1, and the quartiles from R’s default quantile (type 7). The constant is 0.9, which is bw.nrd0, not the 1.06 of bw.nrd. It is a normal-reference rule, not the minimiser of either error below. The generator that builds the sample is not R’s generator. The seed reproduces this page, not an R session.
What the picture is showing
Panel A is undersmoothing against oversmoothing. A small h leaves a bump on the observations: the curve is irregular, integrated variance is high, and integrated squared bias is low. A large h averages over a wide window. Variance falls and bias rises. On the mixture the two modes move together and can disappear.
Panel B plots that split against h. Integrated variance and integrated squared bias add to the mean integrated squared error,
\mathrm{MISE}(h)=\mathrm{IVar}(h)+\mathrm{IBias}^2(h)=\E\int_{-\infty}^{\infty}\bigl(\widehat f_h(x)-f(x)\bigr)^2\,\mathrm{d}x.
The fourth curve is the integrated squared error of the one sample on the screen. For a Gaussian kernel and a normal or Gaussian-mixture f, both errors are the closed form that comes from \int \varphi_a(x-u)\varphi_b(x-v)\,\mathrm{d}x=\varphi_{\sqrt{a^2+b^2}}(u-v). They are not grid approximations. A new sample moves the ISE curve and leaves the other three where they are.
Where this fails
Both numbers use the true f. A sample on its own does not provide them, so the bandwidth that minimises MISE cannot be read off the data, and the bandwidth that minimises this sample’s ISE is not a substitute for it. ISE can sit far from MISE. Silverman’s rule fills in a normal shape. On the mixture it is often wide enough to blur the modes.
The log-likelihood of the sample that built \widehat f_h is a worse guide: it goes to infinity as h\to 0, because each observation sits under its own spike. The slider does not choose h that way.
If every draw is equal, bw.nrd0 has no spread to scale and falls back to a width of order n^{-1/5}. The estimate is then one bump. It does not recover a mixture. A sample of size 0 is not an estimate. A sample of size 1 has an estimate and an ISE, and it does not have a bw.nrd0 bandwidth. If a computed error is not finite, the figure says so and does not replace it.
On the theory page
- Bias and variance in one bandwidth, Choosing the bandwidth, and the kernel density estimator.
- Worked questions: Kernel density by hand, A tempting wrong turn, and Clustering and smoothing choices.