Bias Variance Viz

Drag a single complexity slider. The left plot shows a family of polynomial fits drawn from independent noisy samples of a hidden target function — the spread between fits is the variance, the gap between their mean and the dashed target is the bias. The right plot precomputes bias², variance, and total error across the whole complexity grid so the U-shape of statistical learning is always visible; a guideline tracks the slider.

Bias-Variance Tradeoffdegree 07 · variance-dominated
0.000.250.500.751.000.000.010.030.040.060.000.250.500.751.00complexityx
bias² = 0.000variance = 0.004total = 0.014σ = 0.10
0.50 · deg 7
Customize
Model
0.50
Data
0.10
8

Installation

npx shadcn@latest add https://craftbits.dev/r/bias-variance-viz.json

Usage

import { BiasVarianceViz } from "@craft-bits/core";
 
<BiasVarianceViz defaultComplexity={0.5} />

Control complexity from outside:

const [c, setC] = useState(0.5);
 
<BiasVarianceViz
  complexity={c}
  onComplexityChange={setC}
  noiseLevel={0.15}
  nModels={12}
/>

Swap the target function for a different learning problem:

<BiasVarianceViz
  targetFn={(x) => Math.cos(2 * Math.PI * x) * 0.6 + 0.5 * x}
  defaultComplexity={0.5}
/>

Understanding the component

  1. Complexity drives polynomial degree. complexity is a [0, 1] slider mapped to polynomial degree floor(1 + c · 12) — so 0 is a straight line and 1 is a degree-13 polynomial that has more than enough flexibility to overfit the 14 training points.
  2. Left plot — variance as spread. nModels independent noisy datasets are sampled from targetFn (each with the same noiseLevel Gaussian observation noise), and a polynomial of the current degree is fit to each. The fits are drawn in accent at 30% opacity. High variance fans them out; low variance collapses them onto one curve.
  3. Left plot — bias as gap. The mean of all fits is drawn as a solid accent line. Its distance from the dashed target function is the bias. Low-degree polynomials are too rigid to follow the target; high-degree polynomials follow it faithfully on average but fan wide.
  4. Right plot — the U-shaped error curve. Bias², variance, and total error are precomputed at 21 complexity values using a Monte Carlo estimate over a 32-point test grid. Bias² falls as degree grows; variance rises; the sum is the classic U — the sweet spot is the dip.
  5. Deterministic sampling. Random draws come from a seeded Mulberry32 PRNG. Plots are stable across renders and SSR.
  6. Spring transitions. Every curve animates between complexity values with SPRINGS.smooth. prefers-reduced-motion: reduce collapses every animation to duration: 0.

Props

PropTypeDefaultDescription
complexitynumberControlled complexity in [0, 1].
defaultComplexitynumber0.4Uncontrolled initial complexity.
onComplexityChange(complexity: number) => voidFires on every slider drag.
targetFn(x: number) => numberMath.sin(x · π)Ground-truth function sampled on [0, 1].
noiseLevelnumber0.1Standard deviation of observation noise added to training samples.
nModelsnumber8Number of polynomial fits drawn at the current complexity.
transitionTransitionSPRINGS.smoothSpring for the right-plot guideline.
classNamestringMerged onto the root <div> via cn().

Accessibility

  • The whole visualization is role="figure" with an aria-labelledby heading.
  • The two SVG canvases share a role="img" label; the textual status is in an aria-live="polite" region.
  • The complexity slider is the standard LabeledSlider — native <input type="range"> with full keyboard support (arrows, page-up/down, home/end).
  • Animation respects prefers-reduced-motion: reduce — every spring collapses to an instant swap.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/nn/BiasVarianceViz.tsx). The original was a dartboard metaphor with three preset modes and per-mode narration; this rewrite replaces it with the canonical twin-plot tradeoff diagram — fit family on the left, U-shaped error curve on the right — and exposes a single continuous complexity slider as the only control.