Joint Laws, Independence, Dependence, and the Multivariate Normal
// ---------------------------------------------------------------------------
// ldk — a tiny, dependency-free plotting + statistics library for these decks.
//
// Everything runs in the browser as plain JavaScript building SVG nodes, so a
// slider move costs a fraction of a millisecond instead of a round trip
// through a WebAssembly R runtime.
// ---------------------------------------------------------------------------
ldk = {
const NS = "http://www.w3.org/2000/svg";
let uid = 0;
// Deck palette, matching ldkslides.css
const C = {
green: "#74b83d",
blue: "#4072c2",
pink: "#ED017D",
orange: "#d18b2a",
grey: "#6E6E6E",
ink: "#222222",
axis: "#9aa0a6",
faint: "#efefef"
};
function el(tag, attrs, text) {
const e = document.createElementNS(NS, tag);
if (attrs) for (const k in attrs) if (attrs[k] != null) e.setAttribute(k, attrs[k]);
if (text != null)
// SVG collapses runs of whitespace, which would eat the wide gaps that
// separate the readout columns; non-breaking spaces survive. (xml:space
// is a namespaced attribute and does not survive setAttribute here.)
e.textContent = tag === "text"
? String(text).replace(/ {2,}/g, m => " ".repeat(m.length))
: text;
return e;
}
function alpha(hex, a) {
const n = parseInt(hex.slice(1), 16);
return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`;
}
// ------------------------------------------------------------------ numerics
const LGC = [676.5203681218851, -1259.1392167224028, 771.32342877765313,
-176.61502916214059, 12.507343278686905, -0.13857109526572012,
9.9843695780195716e-6, 1.5056327351493116e-7];
function lgamma(z) {
if (z < 0.5) return Math.log(Math.PI / Math.sin(Math.PI * z)) - lgamma(1 - z);
z -= 1;
let x = 0.99999999999980993;
for (let i = 0; i < 8; i++) x += LGC[i] / (z + i + 1);
const t = z + 7.5;
return 0.5 * Math.log(2 * Math.PI) + (z + 0.5) * Math.log(t) - t + Math.log(x);
}
const lchoose = (n, k) => lgamma(n + 1) - lgamma(k + 1) - lgamma(n - k + 1);
// Chebyshev erfc (Numerical Recipes); plenty of accuracy for plotting.
const ERFC_COF = [
-1.3026537197817094, 6.4196979235649026e-1, 1.9476473204185836e-2,
-9.561514786808631e-3, -9.46595344482036e-4, 3.66839497852761e-4,
4.2523324806907e-5, -2.0278578112534e-5, -1.624290004647e-6,
1.303655835580e-6, 1.5626441722e-8, -8.5238095915e-8,
6.529054439e-9, 5.059343495e-9, -9.91364156e-10, -2.27365122e-10,
9.6467911e-11, 2.394038e-12, -6.886027e-12, 8.94487e-13,
3.13092e-13, -1.12708e-13, 3.81e-16, 7.106e-15];
function erfc(x) {
const z = Math.abs(x), t = 2 / (2 + z), ty = 4 * t - 2;
let d = 0, dd = 0;
for (let j = ERFC_COF.length - 1; j > 0; j--) {
const tmp = d;
d = ty * d - dd + ERFC_COF[j];
dd = tmp;
}
const ans = t * Math.exp(-z * z + 0.5 * (ERFC_COF[0] + ty * d) - dd);
return x >= 0 ? ans : 2 - ans;
}
const dnorm = (x, m = 0, s = 1) => {
const z = (x - m) / s;
return Math.exp(-0.5 * z * z) / (s * Math.sqrt(2 * Math.PI));
};
const pnorm = (x, m = 0, s = 1) => 0.5 * erfc(-(x - m) / (s * Math.SQRT2));
// Acklam's inverse normal CDF (relative error < 1.2e-9)
const QN_A = [-3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02,
1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00];
const QN_B = [-5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02,
6.680131188771972e+01, -1.328068155288572e+01];
const QN_C = [-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00,
-2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00];
const QN_D = [7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00,
3.754408661907416e+00];
function qnorm(p, m = 0, s = 1) {
if (p <= 0) return -Infinity;
if (p >= 1) return Infinity;
const lo = 0.02425, hi = 1 - lo;
let x, q, r;
if (p < lo) {
q = Math.sqrt(-2 * Math.log(p));
x = (((((QN_C[0] * q + QN_C[1]) * q + QN_C[2]) * q + QN_C[3]) * q + QN_C[4]) * q + QN_C[5]) /
((((QN_D[0] * q + QN_D[1]) * q + QN_D[2]) * q + QN_D[3]) * q + 1);
} else if (p <= hi) {
q = p - 0.5; r = q * q;
x = (((((QN_A[0] * r + QN_A[1]) * r + QN_A[2]) * r + QN_A[3]) * r + QN_A[4]) * r + QN_A[5]) * q /
(((((QN_B[0] * r + QN_B[1]) * r + QN_B[2]) * r + QN_B[3]) * r + QN_B[4]) * r + 1);
} else {
q = Math.sqrt(-2 * Math.log(1 - p));
x = -(((((QN_C[0] * q + QN_C[1]) * q + QN_C[2]) * q + QN_C[3]) * q + QN_C[4]) * q + QN_C[5]) /
((((QN_D[0] * q + QN_D[1]) * q + QN_D[2]) * q + QN_D[3]) * q + 1);
}
return m + s * x;
}
function dbinom(k, n, p) {
if (k < 0 || k > n) return 0;
if (p <= 0) return k === 0 ? 1 : 0;
if (p >= 1) return k === n ? 1 : 0;
return Math.exp(lchoose(n, k) + k * Math.log(p) + (n - k) * Math.log1p(-p));
}
const dpois = (k, lam) => k < 0 ? 0 : Math.exp(-lam + k * Math.log(lam) - lgamma(k + 1));
const dexp = (x, rate) => x < 0 ? 0 : rate * Math.exp(-rate * x);
const pexp = (x, rate) => x <= 0 ? 0 : 1 - Math.exp(-rate * x);
const dunif = (x, a, b) => (x >= a && x <= b) ? 1 / (b - a) : 0;
const punif = (x, a, b) => x <= a ? 0 : x >= b ? 1 : (x - a) / (b - a);
const dlnorm = (x, ml = 0, sl = 1) => x <= 0 ? 0 : dnorm(Math.log(x), ml, sl) / x;
const dchisq1 = (x) => x <= 0 ? 0 : dnorm(Math.sqrt(x)) / Math.sqrt(x);
// ---- regularized lower incomplete gamma P(a,x): series + continued fraction
function gammaP(a, x) {
if (!(x >= 0) || !(a > 0)) return NaN;
if (x === 0) return 0;
const pref = Math.exp(-x + a * Math.log(x) - lgamma(a));
if (x < a + 1) { // series
let ap = a, sum = 1 / a, del = sum;
for (let i = 0; i < 500; i++) {
ap++; del *= x / ap; sum += del;
if (Math.abs(del) < Math.abs(sum) * 1e-16) break;
}
return sum * pref;
}
const TINY = 1e-300; // continued fraction for Q = 1 - P
let b = x + 1 - a, c = 1 / TINY, d = 1 / b, h = d;
for (let i = 1; i <= 500; i++) {
const an = -i * (i - a);
b += 2;
d = an * d + b; if (Math.abs(d) < TINY) d = TINY;
c = b + an / c; if (Math.abs(c) < TINY) c = TINY;
d = 1 / d;
const del = d * c;
h *= del;
if (Math.abs(del - 1) < 1e-16) break;
}
return 1 - pref * h;
}
// ---- regularized incomplete beta I_x(a,b)
function betacf(a, b, x) {
const TINY = 1e-300, qab = a + b, qap = a + 1, qam = a - 1;
let c = 1, d = 1 - qab * x / qap;
if (Math.abs(d) < TINY) d = TINY;
d = 1 / d;
let h = d;
for (let m = 1; m <= 400; m++) {
const m2 = 2 * m;
let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
d = 1 + aa * d; if (Math.abs(d) < TINY) d = TINY;
c = 1 + aa / c; if (Math.abs(c) < TINY) c = TINY;
d = 1 / d; h *= d * c;
aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
d = 1 + aa * d; if (Math.abs(d) < TINY) d = TINY;
c = 1 + aa / c; if (Math.abs(c) < TINY) c = TINY;
d = 1 / d;
const del = d * c;
h *= del;
if (Math.abs(del - 1) < 3e-16) break;
}
return h;
}
function betai(a, b, x) {
if (!(x > 0)) return 0;
if (x >= 1) return 1;
const bt = Math.exp(lgamma(a + b) - lgamma(a) - lgamma(b) +
a * Math.log(x) + b * Math.log1p(-x));
return x < (a + 1) / (a + b + 2)
? bt * betacf(a, b, x) / a
: 1 - bt * betacf(b, a, 1 - x) / b;
}
// ---- chi-squared and Student t
const dchisq = (x, k) => x <= 0 ? 0 :
Math.exp((k / 2 - 1) * Math.log(x) - x / 2 - lgamma(k / 2) - (k / 2) * Math.LN2);
const pchisq = (x, k) => x <= 0 ? 0 : gammaP(k / 2, x / 2);
const dt = (x, nu) => Math.exp(lgamma((nu + 1) / 2) - lgamma(nu / 2)
- 0.5 * Math.log(nu * Math.PI) - (nu + 1) / 2 * Math.log1p(x * x / nu));
const pt = (x, nu) => {
const half = 0.5 * betai(nu / 2, 0.5, nu / (nu + x * x));
return x > 0 ? 1 - half : half;
};
// quantiles by bisection — a handful of calls per redraw, so speed is moot
function invert(cdf, p, lo, hi) {
for (let i = 0; i < 90; i++) {
const mid = (lo + hi) / 2;
if (cdf(mid) < p) lo = mid; else hi = mid;
}
return (lo + hi) / 2;
}
const qchisq = (p, k) => invert(x => pchisq(x, k), p, 0, Math.max(60, k * 25));
const qt = (p, nu) => invert(x => pt(x, nu), p, -1e4, 1e4);
// Cumulative sums of a pmf enumerated over 0..n — cheaper and more accurate
// here than a general incomplete-beta / incomplete-gamma implementation.
function cdfFromPmf(pmf) {
const out = new Array(pmf.length);
let s = 0;
for (let i = 0; i < pmf.length; i++) { s += pmf[i]; out[i] = Math.min(s, 1); }
return out;
}
// ----------------------------------------------------------------- sequences
function seq(a, b, n) {
const out = new Array(n);
for (let i = 0; i < n; i++) out[i] = a + (b - a) * i / (n - 1);
return out;
}
const cumsum = (v) => { let s = 0; return v.map(x => (s += x)); };
const sum = (v) => v.reduce((a, b) => a + b, 0);
const mean = (v) => sum(v) / v.length;
function sd(v) {
const m = mean(v);
return Math.sqrt(v.reduce((a, x) => a + (x - m) * (x - m), 0) / (v.length - 1));
}
function quantileSorted(s, p) {
const h = (s.length - 1) * p, i = Math.floor(h);
const j = Math.min(i + 1, s.length - 1);
return s[i] + (h - i) * (s[j] - s[i]);
}
function hist(v, lo, hi, nb) {
const cnt = new Float64Array(nb), w = (hi - lo) / nb;
for (let i = 0; i < v.length; i++) {
const k = Math.floor((v[i] - lo) / w);
if (k >= 0 && k < nb) cnt[k]++;
else if (v[i] === hi) cnt[nb - 1]++;
}
const dens = new Array(nb);
for (let i = 0; i < nb; i++) dens[i] = cnt[i] / (v.length * w);
return { lo, hi, w, nb, dens, count: cnt };
}
// --------------------------------------------------------------------- rngs
// mulberry32: small, fast, seedable — the demos stay reproducible.
function rng(seed) {
let a = seed >>> 0;
const u = () => {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
let spare = null;
u.normal = () => { // Marsaglia polar
if (spare !== null) { const v = spare; spare = null; return v; }
let x, y, s;
do { x = 2 * u() - 1; y = 2 * u() - 1; s = x * x + y * y; } while (s >= 1 || s === 0);
const f = Math.sqrt(-2 * Math.log(s) / s);
spare = y * f;
return x * f;
};
u.exp = () => -Math.log(1 - u());
u.cauchy = () => Math.tan(Math.PI * (u() - 0.5));
return u;
}
// Weighted sampling without replacement (Efraimidis–Spirakis exponential race)
function sampleWeighted(n, w, u) {
const N = w.length;
const keys = new Array(N);
for (let i = 0; i < N; i++) keys[i] = w[i] > 0 ? -Math.log(1 - u()) / w[i] : Infinity;
const idx = Array.from({ length: N }, (_, i) => i);
idx.sort((a, b) => keys[a] - keys[b]);
return idx.slice(0, n);
}
// Sampling *with* replacement — what the bootstrap needs, and what
// sampleWeighted deliberately is not.
function sampleReplace(n, v, u) {
const out = new Array(n);
for (let i = 0; i < n; i++) out[i] = v[Math.floor(u() * v.length)];
return out;
}
// Beta density — the conjugate posterior for a proportion.
const dbeta = (x, a, b) => (x <= 0 || x >= 1) ? 0 :
Math.exp(lgamma(a + b) - lgamma(a) - lgamma(b) +
(a - 1) * Math.log(x) + (b - 1) * Math.log(1 - x));
// Beta quantile, by inverting the regularized incomplete beta above. Used for
// the Clopper-Pearson interval, whose endpoints are exactly beta quantiles.
const qbeta = (p, a, b) => invert(x => betai(a, b, x), p, 0, 1);
// Gini coefficient of a sorted, non-negative sample: twice the area between
// the Lorenz curve and the diagonal. No closed-form standard error exists,
// which is exactly why the bootstrap earns its place.
function gini(sorted) {
const n = sorted.length;
let num = 0, tot = 0;
for (let i = 0; i < n; i++) { num += (i + 1) * sorted[i]; tot += sorted[i]; }
return tot > 0 ? (2 * num) / (n * tot) - (n + 1) / n : 0;
}
// ------------------------------------------------------- regression + series
// Simple OLS of y on x, with both the classical and the heteroskedasticity-
// robust (HC1) standard error. Deck 08 leans on the pair: the estimate and its
// standard error fail independently, and the standard error fails first.
function ols(x, y) {
const n = x.length;
const mx = mean(x), my = mean(y);
let sxx = 0, sxy = 0;
for (let i = 0; i < n; i++) { const dx = x[i] - mx; sxx += dx * dx; sxy += dx * (y[i] - my); }
const b = sxx > 0 ? sxy / sxx : 0;
const a = my - b * mx;
let rss = 0, meat = 0, tss = 0;
for (let i = 0; i < n; i++) {
const e = y[i] - a - b * x[i], dx = x[i] - mx;
rss += e * e; meat += dx * dx * e * e; tss += (y[i] - my) * (y[i] - my);
}
const se = sxx > 0 && n > 2 ? Math.sqrt(rss / (n - 2) / sxx) : NaN;
const seR = sxx > 0 && n > 2
? Math.sqrt(meat * n / (n - 2)) / sxx // HC1
: NaN;
return { a, b, se, seR, t: b / se, tR: b / seR,
r2: tss > 0 ? 1 - rss / tss : 0, n };
}
const corr = (x, y) => {
const mx = mean(x), my = mean(y);
let sxy = 0, sxx = 0, syy = 0;
for (let i = 0; i < x.length; i++) {
const dx = x[i] - mx, dy = y[i] - my;
sxy += dx * dy; sxx += dx * dx; syy += dy * dy;
}
return sxx > 0 && syy > 0 ? sxy / Math.sqrt(sxx * syy) : 0;
};
// Stationary AR(1), started from its stationary distribution so the first
// observations are not systematically closer to zero than the rest.
function ar1(n, rho, u, sd = 1) {
const out = new Float64Array(n);
let v = u.normal() * sd / Math.sqrt(Math.max(1 - rho * rho, 1e-9));
for (let i = 0; i < n; i++) { v = rho * v + sd * u.normal(); out[i] = v; }
return out;
}
const randomWalk = (n, u, sd = 1) => {
const out = new Float64Array(n);
let v = 0;
for (let i = 0; i < n; i++) { v += sd * u.normal(); out[i] = v; }
return out;
};
// G clusters of m units with intra-cluster correlation rho: a shared group
// draw plus an idiosyncratic one, weighted so the total variance stays 1.
function clustered(G, m, rho, u) {
const out = new Float64Array(G * m);
const sg = Math.sqrt(Math.max(rho, 0)), si = Math.sqrt(Math.max(1 - rho, 0));
for (let g = 0; g < G; g++) {
const shared = sg * u.normal();
for (let i = 0; i < m; i++) out[g * m + i] = shared + si * u.normal();
}
return out;
}
// ---------------------------------------------------------------- formatting
const fmt = (v, d = 2) => (Number.isFinite(v) ? v.toFixed(d) : "—");
const signed = (v, d = 2) => (Number.isFinite(v) ? (v >= 0 ? "+" : "") + v.toFixed(d) : "—");
function smart(v) {
if (!Number.isFinite(v)) return "—";
const a = Math.abs(v);
if (a >= 1e5 || (a > 0 && a < 1e-3)) return v.toExponential(1);
return v.toFixed(a >= 100 ? 0 : a >= 10 ? 1 : 2);
}
function nice(lo, hi, n) {
const span = (hi - lo) || Math.abs(hi) || 1;
const raw = span / n;
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
const norm = raw / mag;
const step = mag * (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10);
const out = [];
for (let v = Math.ceil(lo / step - 1e-9) * step; v <= hi + 1e-9 * span; v += step)
out.push(Math.abs(v) < step * 1e-9 ? 0 : v);
return { ticks: out, step };
}
function logTicks(lo, hi) {
const decades = Math.log10(hi / lo);
const mults = decades > 2.2 ? [1] : decades > 1.1 ? [1, 3] : [1, 2, 5];
const out = [];
for (let e = Math.floor(Math.log10(lo)); e <= Math.ceil(Math.log10(hi)); e++)
for (const k of mults) {
const v = k * Math.pow(10, e);
if (v >= lo * 0.9999 && v <= hi * 1.0001) out.push(v);
}
return out;
}
const decimalsFor = (step) =>
Math.max(0, Math.min(6, -Math.floor(Math.log10(Math.abs(step)) + 1e-9)));
// -------------------------------------------------------------------- charts
function chart(W, H, extra) {
const svg = el("svg", Object.assign({
viewBox: `0 0 ${W} ${H}`,
style: `width:100%;height:auto;display:block;font-family:inherit;color:${C.ink}`
}, extra || {}));
svg.__W = W;
svg.__H = H;
return svg;
}
function panel(svg, o) {
const L = o.left ?? 0, T = o.top ?? 0;
const W = o.w ?? svg.__W, H = o.h ?? svg.__H;
const m = Object.assign({ l: 74, r: 22, t: 52, b: 54 }, o.margin || {});
const fs = o.fontSize ?? 15;
const iw = W - m.l - m.r, ih = H - m.t - m.b;
const px0 = L + m.l, py0 = T + m.t;
let [xa, xb] = o.xdom, [ya, yb] = o.ydom;
if (!(xb > xa)) xb = xa + 1;
if (!(yb > ya)) yb = ya + 1;
const lg = o.xlog ? Math.log : (v => v);
const xA = lg(xa), xB = lg(xb);
const x = v => px0 + (lg(v) - xA) / (xB - xA) * iw;
const y = v => py0 + ih - (v - ya) / (yb - ya) * ih;
const gAxis = el("g");
const id = `ldkclip${++uid}`;
const defs = el("defs");
const cp = el("clipPath", { id });
cp.append(el("rect", { x: px0 - 3, y: py0 - 8, width: iw + 6, height: ih + 11 }));
defs.append(cp);
const gc = el("g", { "clip-path": `url(#${id})` });
const gt = el("g");
svg.append(defs, gAxis, gc, gt);
// ticks
let xt, xfmt, yt, yfmt;
if (o.xticks) {
xt = o.xticks;
xfmt = o.xfmt || (v => String(v));
} else if (o.xlog) {
xt = logTicks(xa, xb);
xfmt = o.xfmt || (v => v >= 1 ? String(Math.round(v)) : String(v));
} else {
const n = nice(xa, xb, o.nx ?? 6);
xt = n.ticks;
xfmt = o.xfmt || (v => v.toFixed(decimalsFor(n.step)));
}
if (o.yticks) {
yt = o.yticks;
yfmt = o.yfmt || (v => String(v));
} else {
const n = nice(ya, yb, o.ny ?? 5);
yt = n.ticks;
yfmt = o.yfmt || (v => v.toFixed(decimalsFor(n.step)));
}
if (o.axes === false) { xt = []; yt = []; } // bare drawing surface
if (o.axes !== false) {
gAxis.append(el("line", { x1: px0, y1: py0 + ih, x2: px0 + iw, y2: py0 + ih,
stroke: C.axis, "stroke-width": 1.3 }));
gAxis.append(el("line", { x1: px0, y1: py0, x2: px0, y2: py0 + ih,
stroke: C.axis, "stroke-width": 1.3 }));
}
for (const t of xt) {
const px = x(t);
if (px < px0 - 0.5 || px > px0 + iw + 0.5) continue;
gAxis.append(el("line", { x1: px, y1: py0 + ih, x2: px, y2: py0 + ih + 5,
stroke: C.axis, "stroke-width": 1.3 }));
gAxis.append(el("text", { x: px, y: py0 + ih + 6 + fs, "text-anchor": "middle",
"font-size": fs, fill: C.grey }, xfmt(t)));
}
for (const t of yt) {
const py = y(t);
if (py < py0 - 0.5 || py > py0 + ih + 0.5) continue;
gAxis.append(el("line", { x1: px0 - 5, y1: py, x2: px0, y2: py,
stroke: C.axis, "stroke-width": 1.3 }));
gAxis.append(el("text", { x: px0 - 10, y: py + fs * 0.35, "text-anchor": "end",
"font-size": fs, fill: C.grey }, yfmt(t)));
}
if (o.xlab)
gAxis.append(el("text", { x: px0 + iw / 2, y: T + H - 8, "text-anchor": "middle",
"font-size": fs + 2, fill: C.ink }, o.xlab));
if (o.ylab) {
const ty = py0 + ih / 2, tx = L + 20;
gAxis.append(el("text", { x: tx, y: ty, "text-anchor": "middle", "font-size": fs + 2,
fill: C.ink, transform: `rotate(-90 ${tx} ${ty})` }, o.ylab));
}
const P = {
x, y, svg, gAxis, gc, gt, fs,
xdom: [xa, xb], ydom: [ya, yb],
area: { x0: px0, y0: py0, w: iw, h: ih },
add(node, top) { (top ? gt : gc).append(node); return node; },
path(pts, a) {
let d = "";
for (let i = 0; i < pts.length; i++)
d += (i ? "L" : "M") + x(pts[i][0]).toFixed(2) + "," + y(pts[i][1]).toFixed(2);
return P.add(el("path", Object.assign(
{ d, fill: "none", "stroke-linejoin": "round", "stroke-linecap": "round" }, a)));
},
fill(pts, base, a) {
let d = "";
for (let i = 0; i < pts.length; i++)
d += (i ? "L" : "M") + x(pts[i][0]).toFixed(2) + "," + y(pts[i][1]).toFixed(2);
d += "L" + x(pts[pts.length - 1][0]).toFixed(2) + "," + y(base).toFixed(2);
d += "L" + x(pts[0][0]).toFixed(2) + "," + y(base).toFixed(2) + "Z";
return P.add(el("path", Object.assign({ d, stroke: "none" }, a)));
},
seg(x1, y1, x2, y2, a) {
return P.add(el("line", Object.assign(
{ x1: x(x1), y1: y(y1), x2: x(x2), y2: y(y2) }, a)));
},
vline(v, a) { return P.seg(v, ya, v, yb, a); },
hline(v, a) { return P.seg(xa, v, xb, v, a); },
bar(x1, x2, y1, y2, a) {
const X1 = x(x1), X2 = x(x2), Y1 = y(y1), Y2 = y(y2);
return P.add(el("rect", Object.assign({
x: Math.min(X1, X2), y: Math.min(Y1, Y2),
width: Math.max(Math.abs(X2 - X1), 0.5),
height: Math.max(Math.abs(Y2 - Y1), 0)
}, a)));
},
dot(vx, vy, r, a) {
return P.add(el("circle", Object.assign({ cx: x(vx), cy: y(vy), r }, a)));
},
// mtext-style annotation above the plot region
header(s, a = {}) {
const adj = a.adj ?? 0;
const py = py0 - 14 - (a.line ?? 0) * (fs + 7);
return P.add(el("text", {
x: adj === 1 ? px0 + iw : adj === 0.5 ? px0 + iw / 2 : px0,
y: py,
"text-anchor": adj === 1 ? "end" : adj === 0.5 ? "middle" : "start",
"font-size": a.size ?? fs + 2,
"font-weight": a.weight ?? "bold",
fill: a.fill ?? C.ink
}, s), true);
},
legend(items, opt = {}) {
const size = opt.size ?? fs;
const pad = opt.pad ?? 8;
const rowH = size + 8, swW = 30;
const g = el("g");
let maxw = 0;
items.forEach((it, i) => {
const ty = i * rowH + size;
if (it.swatch)
g.append(el("rect", { x: 0, y: ty - size * 0.82, width: swW,
height: size * 0.85, fill: it.swatch, stroke: "none" }));
else
g.append(el("line", { x1: 0, y1: ty - size * 0.35, x2: swW, y2: ty - size * 0.35,
stroke: it.color, "stroke-width": it.lw ?? 3.5,
"stroke-dasharray": it.dash ?? null,
"stroke-linecap": "round" }));
g.append(el("text", { x: swW + 9, y: ty, "font-size": size,
fill: opt.fill ?? C.ink }, it.label));
maxw = Math.max(maxw, swW + 9 + it.label.length * size * 0.54);
});
// opt-in backing panel, for legends that must sit over the data
if (opt.bg) {
const q = 8;
g.insertBefore(el("rect", {
x: -q, y: -q + 3, width: maxw + 2 * q,
height: items.length * rowH + 2 * q - 6,
fill: "white", "fill-opacity": opt.bg === true ? 0.85 : opt.bg, rx: 5
}), g.firstChild);
}
const corner = opt.corner ?? "topright";
const gx = corner.includes("right") ? px0 + iw - maxw - pad : px0 + pad;
const gy = corner.includes("top") ? py0 + pad
: py0 + ih - items.length * rowH - pad;
g.setAttribute("transform", `translate(${gx},${gy})`);
gt.append(g);
return g;
}
};
return P;
}
// Convenience: one chart with exactly one panel filling it.
function single(W, H, o) {
const svg = chart(W, H);
return { svg, p: panel(svg, o) };
}
return {
C, el, alpha, chart, panel, single,
lgamma, lchoose, erfc, dnorm, pnorm, qnorm, dbinom, dpois, dexp, pexp,
dunif, punif, dlnorm, dchisq1, cdfFromPmf,
gammaP, betai, dbeta, qbeta, dchisq, pchisq, qchisq, dt, pt, qt, invert,
gini,
seq, cumsum, sum, mean, sd, quantileSorted, hist, rng, sampleWeighted,
sampleReplace, ols, corr, ar1, randomWalk, clustered,
fmt, signed, smart, nice, logTicks
};
}Definition — Random Vector
A multivariate random variable (or random vector) is a function \[\mathbf{X}: \Omega \to \mathbb{R}^n\] where \(\Omega\) is the sample space and \(n \geq 2\).
We write \(\mathbf{X} = (X_1, X_2, \ldots, X_n)^T\), where each \(X_i\) is a univariate random variable.
Examples
Definition — Joint CDF
The joint cumulative distribution function of \(\mathbf{X} = (X_1, \ldots, X_n)\) is \[F_{\mathbf{X}}(x_1, \ldots, x_n) = P(X_1 \leq x_1, \ldots, X_n \leq x_n)\]
Properties:
Why the joint CDF matters
Exactly as in one dimension, it is the one object that always exists — discrete, continuous, or mixed.
For discrete random vectors \(\mathbf{X} = (X_1, \ldots, X_n)\):
Definition — Joint PMF
\[p_{\mathbf{X}}(x_1, \ldots, x_n) = P(X_1 = x_1, \ldots, X_n = x_n)\]
Properties:
For continuous random vectors \(\mathbf{X} = (X_1, \ldots, X_n)\):
Definition — Joint PDF
The joint density \(f_{\mathbf{X}}\) satisfies \[F_{\mathbf{X}}(x_1, \ldots, x_n) = \int_{-\infty}^{x_1} \cdots \int_{-\infty}^{x_n} f_{\mathbf{X}}(t_1, \ldots, t_n) \, dt_n \cdots dt_1\]
Properties:
Definition — Marginal Distribution
The marginal distribution of \(X_i\) is the distribution of \(X_i\) alone, obtained by summing or integrating the others away.
Discrete case (PMF): \[p_{X_i}(x_i) = \sum_{x_1} \cdots \sum_{x_{i-1}} \sum_{x_{i+1}} \cdots \sum_{x_n} p_{\mathbf{X}}(x_1, \ldots, x_n)\]
Continuous case (PDF): \[f_{X_i}(x_i) = \int_{-\infty}^{\infty} \cdots \int_{-\infty}^{\infty} f_{\mathbf{X}}(x_1, \ldots, x_n) \, dx_1 \cdots dx_{i-1} \, dx_{i+1} \cdots dx_n\]
For any subset of the variables, e.g. \((X_1, X_2)\) from \((X_1, X_2, X_3)\):
Discrete: \[p_{X_1, X_2}(x_1, x_2) = \sum_{x_3} p_{X_1, X_2, X_3}(x_1, x_2, x_3)\]
Continuous: \[f_{X_1, X_2}(x_1, x_2) = \int_{-\infty}^{\infty} f_{X_1, X_2, X_3}(x_1, x_2, x_3) \, dx_3\]
One-way street
The joint distribution determines all marginals. The marginals do not determine the joint — as the live demo two slides on will show.
Given \(\mathbf{X} = (X_1, \ldots, X_k)\) and \(\mathbf{Y} = (Y_1, \ldots, Y_m)\):
Conditional Densities
If \(\mathbf{X}\) and \(\mathbf{Y}\) are dependent, learning \(\mathbf{Y} = \mathbf{y}\) updates the uncertainty about \(\mathbf{X}\):
\[p_{\mathbf{X}|\mathbf{Y}}(\mathbf{x}|\mathbf{y}) = \frac{p_{\mathbf{X},\mathbf{Y}}(\mathbf{x}, \mathbf{y})}{p_{\mathbf{Y}}(\mathbf{y})} \quad\text{or}\quad f_{\mathbf{X}|\mathbf{Y}}(\mathbf{x}|\mathbf{y}) = \frac{f_{\mathbf{X},\mathbf{Y}}(\mathbf{x}, \mathbf{y})}{f_{\mathbf{Y}}(\mathbf{y})}\]
Properties:
The joint sits in the middle; the margins are what you get by integrating out, the slice is what you get by conditioning.
Standard bivariate normal. The top curve is the marginal of \(X\); the right panel shows the marginal of \(Y\) (grey) against the conditional \(Y \mid X = x_0\) (blue).
The marginal of \(Y\) is \(N(0,1)\) whatever \(\rho\) is — only the joint changes.
{
const C = ldk.C, r = bn_rho, x0 = bn_x0;
const s = Math.sqrt(1 - r * r); // sd of Y given X
const mCond = r * x0; // E[Y | X = x0]
const L = 3.7;
const svg = ldk.chart(900, 560);
// contour ellipses: (x,y)^T Sigma^{-1} (x,y) = k^2 is exactly an ellipse,
// so no marching squares are needed — parametrise it directly
const ell = (k, N = 160) => {
const pts = [];
for (let i = 0; i <= N; i++) {
const t = 2 * Math.PI * i / N;
pts.push([k * Math.cos(t), k * (r * Math.cos(t) + s * Math.sin(t))]);
}
return pts;
};
// ---- main panel: the joint density
const pm = ldk.panel(svg, {
left: 0, top: 160, w: 620, h: 400, xdom: [-L, L], ydom: [-L, L],
xlab: "x", ylab: "y", margin: { l: 76, r: 14, t: 6, b: 56 }, nx: 5, ny: 5
});
for (let k = 3.2; k > 0.12; k -= 0.2)
pm.add(ldk.el("path", {
d: ell(k).map((p, i) => (i ? "L" : "M") + pm.x(p[0]).toFixed(1) + "," +
pm.y(p[1]).toFixed(1)).join("") + "Z",
fill: ldk.alpha(C.blue, 0.07), stroke: "none" }));
for (const k of [1, 2, 3])
pm.path(ell(k), { stroke: ldk.alpha(C.blue, 0.85), "stroke-width": 2 });
// the conditional mean as a function of x is the regression line E[Y|X=x] = rho x
pm.path([[-L, -r * L], [L, r * L]],
{ stroke: C.green, "stroke-width": 2.5, "stroke-dasharray": "8,6" });
pm.seg(x0, -L, x0, L, { stroke: C.pink, "stroke-width": 2.5 });
pm.dot(x0, mCond, 6, { fill: C.pink });
// white halo: the conditioning line can cross this label at any x0
pm.add(ldk.el("text", { x: pm.x(L) - 6, y: pm.y(r * L) - 10, "text-anchor": "end",
"font-size": 15, "font-weight": "bold", fill: C.green,
stroke: "white", "stroke-width": 4, "paint-order": "stroke" },
"E[Y | X = x] = ρx"), true);
// ---- top panel: the marginal of X (also carries the readout headers, since
// the main panel is butted right up against it)
const gx = ldk.seq(-L, L, 400);
const pt = ldk.panel(svg, {
left: 0, top: 0, w: 620, h: 160, xdom: [-L, L], ydom: [0, 0.46],
xticks: [], ylab: "f(x)", margin: { l: 76, r: 14, t: 62, b: 6 }, ny: 2
});
pt.fill(gx.map(v => [v, ldk.dnorm(v)]), 0, { fill: ldk.alpha(C.grey, 0.18) });
pt.path(gx.map(v => [v, ldk.dnorm(v)]), { stroke: C.grey, "stroke-width": 3 });
pt.seg(x0, 0, x0, 0.46, { stroke: C.pink, "stroke-width": 2.5 });
pt.header(`E[Y | X = ${ldk.fmt(x0, 2)}] = ρx₀ = ${ldk.fmt(mCond, 3)}` +
` sd(Y | X) = √(1 − ρ²) = ${ldk.fmt(s, 3)}`,
{ line: 1, size: 18, fill: C.pink });
pt.header("marginal of X — N(0,1) for every ρ", { size: 16, fill: C.grey });
// ---- right panel: marginal of Y against the conditional slice
const dmax = 0.42 / s;
const pr = ldk.panel(svg, {
left: 620, top: 160, w: 280, h: 400, xdom: [0, dmax], ydom: [-L, L],
yticks: [], xlab: "density", margin: { l: 12, r: 40, t: 6, b: 56 }, nx: 3
});
const gy = ldk.seq(-L, L, 400);
// closing this fill runs along x = 0, not y = base, so build the polygon by hand
const cond = gy.map(v => [ldk.dnorm(v, mCond, s), v]);
pr.add(ldk.el("path", {
d: cond.map((q, i) => (i ? "L" : "M") + pr.x(q[0]).toFixed(2) + "," +
pr.y(q[1]).toFixed(2)).join("") +
`L${pr.x(0).toFixed(2)},${pr.y(L).toFixed(2)}` +
`L${pr.x(0).toFixed(2)},${pr.y(-L).toFixed(2)}Z`,
fill: ldk.alpha(C.blue, 0.18), stroke: "none" }));
pr.path(gy.map(v => [ldk.dnorm(v), v]), { stroke: C.grey, "stroke-width": 3,
"stroke-dasharray": "8,6" });
pr.path(cond, { stroke: C.blue, "stroke-width": 3.5 });
pr.seg(0, mCond, dmax, mCond, { stroke: C.pink, "stroke-width": 2,
"stroke-dasharray": "4,5" });
pr.legend([
{ label: "marginal of Y", color: C.grey, lw: 3, dash: "8,6" },
{ label: "Y | X = x₀", color: C.blue, lw: 3.5 }
], { size: 13, corner: "topright", bg: true });
return svg;
}Conditional Moments
\[\begin{aligned} E[\mathbf{X}\mid\mathbf{Y} = \mathbf{y}] &= \int \mathbf{x} \, f_{\mathbf{X}|\mathbf{Y}}(\mathbf{x}|\mathbf{y}) \, d\mathbf{x}\\ \text{Var}[\mathbf{X}\mid\mathbf{Y} = \mathbf{y}] &= E[\mathbf{X}\mathbf{X}^T\mid\mathbf{Y} = \mathbf{y}] - E[\mathbf{X}\mid\mathbf{Y} = \mathbf{y}]\,E[\mathbf{X}\mid\mathbf{Y} = \mathbf{y}]^T \end{aligned}\]
If \(\mathbf{X}\) and \(\mathbf{Y}\) are independent, then for every \(\mathbf{y}\)
Law of Total Expectation
\[E[\mathbf{X}] = E_{\mathbf{Y}}\big[E_{\mathbf{X}|\mathbf{Y}}[\mathbf{X}\mid\mathbf{Y}]\big] = \int_{\mathbf{Y}} \left[\int_{\mathbf{X}|\mathbf{Y}} \mathbf{x}\, f_{\mathbf{X}|\mathbf{Y}}(\mathbf{x}|\mathbf{y}) \, d\mathbf{x}\right] f_{\mathbf{Y}}(\mathbf{y}) \, d\mathbf{y}\]
Definition — Mutual Independence
\(X_1, \ldots, X_n\) are mutually independent if \[F_{\mathbf{X}}(x_1, \ldots, x_n) = F_{X_1}(x_1) \cdot F_{X_2}(x_2) \cdots F_{X_n}(x_n)\] for all \((x_1, \ldots, x_n) \in \mathbb{R}^n\).
Equivalent conditions:
Under independence the joint is the product of the marginals — the one case where the marginals do determine the joint.
Pairwise Independence
\(X_1, \ldots, X_n\) are pairwise independent if \(X_i\) and \(X_j\) are independent for every \(i \neq j\).
Mutual Independence
\(X_1, \ldots, X_n\) are mutually independent if every subset \(\{X_{i_1}, \ldots, X_{i_k}\}\) consists of independent variables.
Key Point
Mutual independence is a strictly stronger condition.
\[\text{Mutual independence} \implies \text{Pairwise independence}\] \[\text{Pairwise independence} \;\not\!\!\!\implies \text{Mutual independence}\]
Toss a fair coin three times and define
\[\begin{aligned} A &: \text{the first coin is heads}\\ B &: \text{the second coin is heads}\\ C &: \text{the first two coins agree} \end{aligned}\]
Sample space: \(\{HHH, HHT, HTH, HTT, THH, THT, TTH, TTT\}\), all equally likely.
Every pair multiplies correctly: \(P(A \cap B) = P(A \cap C) = P(B \cap C) = 1/4\).
All three at once:
\[A \cap B \cap C = \{HHH, HHT\}, \qquad P(A \cap B \cap C) = \tfrac{2}{8} = \tfrac{1}{4}\]
But mutual independence would require
\[P(A \cap B \cap C) = P(A)P(B)P(C) = \tfrac{1}{2}\cdot\tfrac{1}{2}\cdot\tfrac{1}{2} = \tfrac{1}{8}\]
Conclusion
Since \(\tfrac{1}{4} \neq \tfrac{1}{8}\), the three events are pairwise independent but not mutually independent.
Every pair checks out. The triple does not. Step through them.
Eight equally likely outcomes. A column is shaded in a row when that outcome belongs to the event. The bottom row is the intersection of everything you selected — count its squares and compare with the product of the probabilities.
{
const C = ldk.C;
const OUT = ["HHH", "HHT", "HTH", "HTT", "THH", "THT", "TTH", "TTT"];
// A: first is H; B: second is H; C: first two agree
const EV = {
A: OUT.map(o => o[0] === "H"),
B: OUT.map(o => o[1] === "H"),
C: OUT.map(o => o[0] === o[1])
};
const picked = ci_sel.split("");
const inAll = OUT.map((_, i) => picked.every(e => EV[e][i]));
const nAll = inAll.filter(Boolean).length;
const pJoint = nAll / 8;
const pProd = Math.pow(0.5, picked.length);
const agree = Math.abs(pJoint - pProd) < 1e-12;
const COL = { A: C.blue, B: C.green, C: C.orange };
const svg = ldk.chart(900, 520);
const rows = ["A", "B", "C"];
const p = ldk.panel(svg, {
xdom: [-1.35, 8], ydom: [0, 5.4], axes: false,
margin: { l: 30, r: 20, t: 80, b: 40 }
});
// outcome labels
OUT.forEach((o, i) => p.add(ldk.el("text", {
x: p.x(i + 0.5), y: p.y(4.85), "text-anchor": "middle", "font-size": 16,
"font-weight": "bold", fill: C.grey }, o), true));
rows.forEach((e, ri) => {
const y = 3.9 - ri * 0.95;
p.add(ldk.el("text", { x: p.x(-0.35), y: p.y(y - 0.42) + 6, "text-anchor": "end",
"font-size": 20, "font-weight": "bold",
fill: picked.includes(e) ? COL[e] : "#c9c9c9" }, e), true);
OUT.forEach((_, i) => {
const on = EV[e][i], live = picked.includes(e);
p.bar(i + 0.06, i + 0.94, y - 0.82, y - 0.02, {
fill: on ? (live ? ldk.alpha(COL[e], 0.75) : "#e9e9e9") : "#f6f6f6",
stroke: "white", "stroke-width": 1.5 });
});
});
// the intersection row
const yi = 0.55;
p.add(ldk.el("text", { x: p.x(-0.35), y: p.y(yi - 0.42) + 6, "text-anchor": "end",
"font-size": 20, "font-weight": "bold", fill: C.pink },
"∩"), true);
OUT.forEach((_, i) => {
p.bar(i + 0.06, i + 0.94, yi - 0.82, yi - 0.02, {
fill: inAll[i] ? ldk.alpha(C.pink, 0.8) : "#f6f6f6",
stroke: "white", "stroke-width": 1.5 });
});
p.header(`P(${picked.join(" ∩ ")}) = ${nAll}/8 = ${ldk.fmt(pJoint, 3)}` +
` ${picked.map(e => "P(" + e + ")").join(" · ")} = ` +
`${ldk.fmt(pProd, 3)}`, { line: 1, size: 19 });
p.header(agree ? "the two agree — this collection is independent"
: "the two disagree — NOT independent, even though every pair is",
{ size: 19, fill: agree ? C.green : C.pink });
return svg;
}Where this bites
The distinction matters wherever a product of probabilities is taken over more than two things at once — likelihoods, hashing arguments, and independence assumptions in machine learning models.
Mean Dependence
\[E[Y \mid X = x] \neq E[Y]\] The conditional expectation of \(Y\) depends on the value of \(X\).
Quantile Dependence
\[Q_{Y \mid X = x}(\tau) \ \text{ depends on } x \quad \text{for some } \tau \in (0,1)\] The conditional quantiles move with \(X\) even when the conditional mean does not — dependence in spread or shape, not in level.
Tail Dependence
Extreme values occur together. For the upper tail, \[\lambda_U = \lim_{\alpha \to 1^-} P\big(Y > Q_Y(\alpha) \mid X > Q_X(\alpha)\big)\]
Ways \(Y\) can depend on \(X\):
Popular alternative measures:
Two separate choices: the dependence structure, and the marginals.
viewof cp_kind = Inputs.select(
new Map([["Independent", "indep"],
["Linear, ρ ≈ +0.8", "pos"],
["Linear, ρ ≈ −0.8", "neg"],
["U-shaped (r ≈ 0!)", "ushape"],
["Tail-dependent, r ≈ 0 (t copula)", "tail"]]),
{label: "Dependence structure", value: "indep"}
)
viewof cp_marg = Inputs.select(
new Map([["Normal", "norm"], ["Uniform", "unif"], ["Exponential (skewed)", "exp"]]),
{label: "Marginals", value: "norm"}
)
viewof cp_view = Inputs.select(
new Map([["Points + density", "both"], ["Points only", "pts"], ["Density only", "dens"]]),
{label: "Show", value: "both"}
)
viewof cp_go = Inputs.button("New sample")The structure sets the ranks — the copula. The marginal only reshapes each axis.
U-shape and tail-dependent: all three measures read \(\approx 0\), yet the pink corners are full. Switch to exponential margins and Pearson jumps to \(\approx +0.4\) — same ranks, different number.
{
const C = ldk.C, N = 600;
const u = ldk.rng(917 + cp_go);
// raw pairs; the marginals are then destroyed by the rank transform, so only
// the copula survives
const rx = new Float64Array(N), ry = new Float64Array(N);
for (let i = 0; i < N; i++) {
const z1 = u.normal(), z2 = u.normal();
let a = z1, b;
if (cp_kind === "indep") b = z2;
else if (cp_kind === "pos") b = 0.8 * z1 + 0.6 * z2;
else if (cp_kind === "neg") b = -0.8 * z1 + 0.6 * z2;
else if (cp_kind === "ushape") b = Math.abs(z1) + 0.28 * z2;
else {
// t copula with rho = 0 and one degree of freedom: a shared random scale
// makes both coordinates big together, but the signs stay independent, so
// E[Y | X] = 0 and every correlation measure reads about zero
const w = Math.abs(u.normal());
a = z1 / w; b = z2 / w;
}
rx[i] = a; ry[i] = b;
}
// rank -> the chosen marginal: the structure decides the ranks, this decides
// what each axis looks like
const MARG = {
norm: { q: p => ldk.qnorm(p), f: v => ldk.dnorm(v), dom: [-3.2, 3.2],
bound: [null, null], lab: "normal" },
unif: { q: p => p, f: v => (v >= 0 && v <= 1 ? 1 : 0), dom: [0, 1],
bound: [0, 1], lab: "uniform" },
exp: { q: p => -Math.log(1 - p), f: v => (v >= 0 ? Math.exp(-v) : 0), dom: [0, 6.8],
bound: [0, null], lab: "exponential" }
}[cp_marg];
const scores = v => {
const idx = Array.from({ length: N }, (_, i) => i).sort((p, q) => v[p] - v[q]);
const out = new Float64Array(N);
idx.forEach((orig, rank) => { out[orig] = MARG.q((rank + 1) / (N + 1)); });
return out;
};
const x = scores(rx), y = scores(ry);
// Pearson, Spearman (Pearson of ranks), Kendall
const pear = (a, b) => {
let ma = 0, mb = 0;
for (let i = 0; i < N; i++) { ma += a[i]; mb += b[i]; }
ma /= N; mb /= N;
let sab = 0, sa = 0, sb = 0;
for (let i = 0; i < N; i++) {
const da = a[i] - ma, db = b[i] - mb;
sab += da * db; sa += da * da; sb += db * db;
}
return sab / Math.sqrt(sa * sb);
};
const rankOf = v => {
const idx = Array.from({ length: N }, (_, i) => i).sort((p, q) => v[p] - v[q]);
const out = new Float64Array(N);
idx.forEach((orig, rank) => { out[orig] = rank; });
return out;
};
let con = 0, dis = 0;
for (let i = 0; i < N; i++)
for (let j = i + 1; j < N; j++) {
const q = (x[i] - x[j]) * (y[i] - y[j]);
if (q > 0) con++; else if (q < 0) dis++;
}
const rP = pear(x, y);
const rS = pear(rankOf(x), rankOf(y));
const tau = (con - dis) / (N * (N - 1) / 2);
const svg = ldk.chart(880, 560);
const [lo, hi] = MARG.dom;
const expected = Math.round(N * 0.01);
// two ways the numbers mislead: all of them blind to the dependence, or
// Pearson disagreeing with the ranks because of the marginals alone
const qLo = MARG.q(0.05), qHi = MARG.q(0.95);
const far = v => v < qLo || v > qHi;
let both = 0;
for (let i = 0; i < N; i++) if (far(x[i]) && far(y[i])) both++;
const blind = Math.max(Math.abs(rP), Math.abs(rS), Math.abs(tau)) < 0.12 &&
both > 2.5 * expected;
const disagree = Math.abs(rP - rS) > 0.2;
const txt = (X, Y, str, a = {}) => svg.append(ldk.el("text", Object.assign(
{ x: X, y: Y, "font-size": 15, fill: C.grey }, a), str));
const measure = (X, name, v, hot) => {
txt(X, 26, name, { "font-size": 17 });
txt(X + 118, 26, ldk.signed(v, 3),
{ "font-size": 17, "font-weight": 700, fill: hot ? C.pink : C.ink });
};
measure(76, "Pearson r", rP, blind || disagree);
measure(266, "Spearman ρ", rS, blind);
measure(456, "Kendall τ", tau, blind);
if (blind || disagree)
txt(76, 48, blind ? "all near zero — yet the extremes arrive together"
: "Pearson disagrees with the ranks: that is the marginal, not the dependence",
{ fill: C.pink, "font-size": 14 });
// ---- the panels: the marginal density on top and to the right, exactly the
// density the ranks were pushed through, and the pair itself in the middle
const fmax = Math.max(...ldk.seq(lo, hi, 400).map(MARG.f)) * 1.18;
const shared = { xdom: [lo, hi], fontSize: 14 };
const dens1 = ldk.seq(lo, hi, 400).map(v => [v, MARG.f(v)]);
const ptop = ldk.panel(svg, Object.assign({
left: 0, top: 56, w: 620, h: 110, ydom: [0, fmax], xticks: [], ylab: "f(x)",
margin: { l: 76, r: 12, t: 6, b: 4 }, ny: 2
}, shared));
ptop.fill(dens1, 0, { fill: ldk.alpha(C.grey, 0.22) });
ptop.path(dens1, { stroke: C.grey, "stroke-width": 2.5 });
const pm = ldk.panel(svg, Object.assign({
left: 0, top: 166, w: 620, h: 394, ydom: [lo, hi],
xlab: "x", ylab: "y", margin: { l: 76, r: 12, t: 6, b: 52 }, nx: 5, ny: 5
}, shared));
const pright = ldk.panel(svg, {
left: 620, top: 166, w: 240, h: 394, xdom: [0, fmax], ydom: [lo, hi],
yticks: [], xlab: "f(y)", margin: { l: 8, r: 36, t: 6, b: 52 }, nx: 2, fontSize: 14
});
// rotated: the density runs along x here, so the shape is closed at x = 0
// by hand (panel.fill would close it at y = 0, which is a value, not a density)
const rot = dens1.map(([v, f]) => [f, v]);
pright.path([[0, lo], ...rot, [0, hi]], { fill: ldk.alpha(C.grey, 0.22), stroke: "none" });
pright.path(rot, { stroke: C.grey, "stroke-width": 2.5 });
// ---- the empirical joint density, by Gaussian KDE. Uniform and exponential
// marginals have hard edges, so points are mirrored across them: without that
// the estimate bleeds out of the support and dents the density at the border.
if (cp_view !== "pts") {
const G = 64;
const gv = ldk.seq(lo, hi, G), step = (hi - lo) / (G - 1);
const hX = 1.06 * ldk.sd(x) * Math.pow(N, -0.2);
const hY = 1.06 * ldk.sd(y) * Math.pow(N, -0.2);
const mirror = v => {
const out = [v];
if (MARG.bound[0] !== null) out.push(2 * MARG.bound[0] - v);
if (MARG.bound[1] !== null) out.push(2 * MARG.bound[1] - v);
return out;
};
const d = new Float64Array(G * G);
const spanX = Math.ceil(4 * hX / step), spanY = Math.ceil(4 * hY / step);
for (let i = 0; i < N; i++)
for (const cx of mirror(x[i])) for (const cy of mirror(y[i])) {
const i0 = Math.round((cx - lo) / step), j0 = Math.round((cy - lo) / step);
for (let a = Math.max(0, i0 - spanX); a <= Math.min(G - 1, i0 + spanX); a++) {
const zx = (gv[a] - cx) / hX;
const wx = Math.exp(-0.5 * zx * zx);
if (wx < 1e-4) continue;
for (let b = Math.max(0, j0 - spanY); b <= Math.min(G - 1, j0 + spanY); b++) {
const zy = (gv[b] - cy) / hY;
d[a * G + b] += wx * Math.exp(-0.5 * zy * zy);
}
}
}
const dmax = Math.max(...d);
// colour ramp: white -> deck blue -> ink, on a square-root scale so the
// thin outer shoulders of the cloud stay visible
const mix = (p, q, t) => p.map((c, k) => Math.round(c + (q[k] - c) * t));
const WHITE = [255, 255, 255], BLUE = [64, 114, 194], DARK = [23, 42, 77];
const colour = t => {
const c = t < 0.55 ? mix(WHITE, BLUE, t / 0.55) : mix(BLUE, DARK, (t - 0.55) / 0.45);
return `rgb(${c[0]},${c[1]},${c[2]})`;
};
const cw = Math.abs(pm.x(gv[1]) - pm.x(gv[0])) + 0.7;
const ch = Math.abs(pm.y(gv[1]) - pm.y(gv[0])) + 0.7;
const heat = pm.add(ldk.el("g", { opacity: cp_view === "both" ? 0.55 : 1 }));
for (let a = 0; a < G; a++)
for (let b = 0; b < G; b++) {
const t = Math.sqrt(d[a * G + b] / dmax);
if (t < 0.02) continue;
heat.append(ldk.el("rect", { x: pm.x(gv[a]) - cw / 2, y: pm.y(gv[b]) - ch / 2,
width: cw, height: ch, fill: colour(t) }));
}
// contour lines, by marching squares on the same grid
const at = (a, b) => d[a * G + b];
const seg = [];
for (const level of [0.15, 0.35, 0.6, 0.85].map(q => q * dmax)) {
for (let a = 0; a < G - 1; a++)
for (let b = 0; b < G - 1; b++) {
const v = [at(a, b), at(a + 1, b), at(a + 1, b + 1), at(a, b + 1)];
const code = (v[0] > level ? 1 : 0) | (v[1] > level ? 2 : 0) |
(v[2] > level ? 4 : 0) | (v[3] > level ? 8 : 0);
if (code === 0 || code === 15) continue;
const ip = (p, q, t) => p + (q - p) * t;
const X0 = pm.x(gv[a]), X1 = pm.x(gv[a + 1]);
const Y0 = pm.y(gv[b]), Y1 = pm.y(gv[b + 1]);
const bot = [ip(X0, X1, (level - v[0]) / (v[1] - v[0])), Y0];
const rgt = [X1, ip(Y0, Y1, (level - v[1]) / (v[2] - v[1]))];
const top = [ip(X0, X1, (level - v[3]) / (v[2] - v[3])), Y1];
const lft = [X0, ip(Y0, Y1, (level - v[0]) / (v[3] - v[0]))];
const pairs = {
1: [[bot, lft]], 2: [[bot, rgt]], 3: [[lft, rgt]], 4: [[rgt, top]],
5: [[bot, rgt], [lft, top]], 6: [[bot, top]], 7: [[lft, top]],
8: [[lft, top]], 9: [[bot, top]], 10: [[bot, lft], [rgt, top]],
11: [[rgt, top]], 12: [[lft, rgt]], 13: [[bot, rgt]], 14: [[bot, lft]]
}[code];
for (const [p1, q1] of pairs) seg.push(`M${p1[0].toFixed(1)},${p1[1].toFixed(1)}L${q1[0].toFixed(1)},${q1[1].toFixed(1)}`);
}
}
pm.add(ldk.el("path", { d: seg.join(""), fill: "none", stroke: "white",
"stroke-width": 1.1, opacity: 0.85 }));
}
// ---- the corners where both coordinates are extreme, and the points
if (cp_view !== "dens") // over the heat map it only muddies
for (const [ax, bx] of [[lo, qLo], [qHi, hi]])
for (const [ay, by] of [[lo, qLo], [qHi, hi]])
pm.bar(ax, bx, ay, by, { fill: ldk.alpha(C.pink, 0.09) });
for (const v of [qLo, qHi]) {
pm.vline(v, { stroke: ldk.alpha(C.pink, 0.5), "stroke-width": 1.5, "stroke-dasharray": "6,5" });
pm.hline(v, { stroke: ldk.alpha(C.pink, 0.5), "stroke-width": 1.5, "stroke-dasharray": "6,5" });
}
if (cp_view !== "dens")
for (let i = 0; i < N; i++) {
const corner = far(x[i]) && far(y[i]);
pm.dot(x[i], y[i], corner ? 3.6 : 3.2,
{ fill: corner ? ldk.alpha(C.pink, 0.95)
: ldk.alpha(C.ink, cp_view === "both" ? 0.55 : 0),
stroke: "none" });
if (!corner && cp_view !== "both")
pm.dot(x[i], y[i], 3.2, { fill: ldk.alpha(C.blue, 0.42), stroke: "none" });
}
// ---- the corner count, in the top-right corner beside the three measures
txt(648, 26, "both coordinates extreme", { "font-size": 14 });
txt(648, 58, `${both} points`, { "font-size": 22, "font-weight": 700, fill: C.pink });
txt(648, 80, `of ${N} · about ${expected} if independent`, { "font-size": 13 });
return svg;
}Definition — Covariance
\[\text{Cov}(X, Y) = E\big[(X - E[X])(Y - E[Y])\big] = E[XY] - E[X]E[Y]\]
Properties:
The converse fails
\(\text{Cov}(X, Y) = 0\) does not imply independence — the U-shaped panel of the last demo is a counterexample.
Definition — Correlation Coefficient
\[\rho(X, Y) = \text{Corr}(X, Y) = \frac{\text{Cov}(X, Y)}{\sqrt{\text{Var}(X)\,\text{Var}(Y)}}\] provided \(\text{Var}(X), \text{Var}(Y) > 0\).
Properties:
For a random vector \(\mathbf{X} = (X_1, \ldots, X_n)^T\):
Definition — Covariance Matrix
\[\boldsymbol{\Sigma} = \text{Cov}(\mathbf{X}) = E\big[(\mathbf{X} - \boldsymbol{\mu})(\mathbf{X} - \boldsymbol{\mu})^T\big], \qquad \boldsymbol{\mu} = E[\mathbf{X}]\] with \((i,j)\)-th element \(\Sigma_{ij} = \text{Cov}(X_i, X_j)\).
Properties:
\(\boldsymbol{\Sigma}\) is an ellipse. Its axes are the eigenvectors; the axis lengths are \(\sqrt{\lambda_i}\).
viewof cv_sx = Inputs.range([0.4, 1.6], {step: 0.01, value: 1, label: "σₓ"})
viewof cv_sy = Inputs.range([0.4, 1.6], {step: 0.01, value: 0.7, label: "σᵧ"})
viewof cv_rho = Inputs.range([-0.99, 0.99], {step: 0.01, value: 0.6, label: "ρ"})
viewof cv_scale = Inputs.range([0.5, 2.5], {step: 0.01, value: 1,
label: "Re-scale X by c"})Changing the units of \(X\) multiplies \(\text{Cov}\) by \(c\) but leaves \(\rho\) alone — that is exactly why correlation, not covariance, is the comparable number.
Push \(|\rho| \to 1\) and the ellipse collapses onto a line: \(\det\boldsymbol{\Sigma} \to 0\), the boundary of positive semidefiniteness.
{
const C = ldk.C;
const c = cv_scale, sx = cv_sx * c, sy = cv_sy, r = cv_rho;
const cov = r * sx * sy, det = sx * sx * sy * sy - cov * cov;
// eigen-decomposition of the 2x2 symmetric Sigma
const a = sx * sx, b = cov, d = sy * sy;
const tr = a + d, disc = Math.sqrt(Math.max((a - d) * (a - d) / 4 + b * b, 0));
const l1 = tr / 2 + disc, l2 = tr / 2 - disc;
const ang = 0.5 * Math.atan2(2 * b, a - d);
const ev = [[Math.cos(ang), Math.sin(ang)], [-Math.sin(ang), Math.cos(ang)]];
const L = 4.6;
const { svg, p } = ldk.single(880, 560, {
xdom: [-L, L], ydom: [-L, L], xlab: "x", ylab: "y", margin: { t: 80 }, nx: 5, ny: 5
});
const s2 = Math.sqrt(Math.max(1 - r * r, 0));
const ell = k => {
const pts = [];
for (let i = 0; i <= 160; i++) {
const t = 2 * Math.PI * i / 160;
pts.push([k * sx * Math.cos(t), k * sy * (r * Math.cos(t) + s2 * Math.sin(t))]);
}
return pts;
};
for (let k = 2.6; k > 0.1; k -= 0.2)
p.add(ldk.el("path", {
d: ell(k).map((q, i) => (i ? "L" : "M") + p.x(q[0]).toFixed(1) + "," +
p.y(q[1]).toFixed(1)).join("") + "Z",
fill: ldk.alpha(C.blue, 0.08), stroke: "none" }));
for (const k of [1, 2])
p.path(ell(k), { stroke: ldk.alpha(C.blue, 0.85), "stroke-width": 2 });
// principal axes, scaled by sqrt(lambda)
[[l1, ev[0], C.pink], [l2, ev[1], C.orange]].forEach(([lam, v, col]) => {
const h = Math.sqrt(Math.max(lam, 0));
p.seg(-h * v[0], -h * v[1], h * v[0], h * v[1],
{ stroke: col, "stroke-width": 3.5 });
p.dot(h * v[0], h * v[1], 5, { fill: col });
});
p.legend([
{ label: `√λ₁ = ${ldk.fmt(Math.sqrt(Math.max(l1, 0)), 3)}`, color: C.pink, lw: 3.5 },
{ label: `√λ₂ = ${ldk.fmt(Math.sqrt(Math.max(l2, 0)), 3)}`, color: C.orange, lw: 3.5 }
], { size: 15, bg: true });
p.header(`Σ = [ ${ldk.fmt(sx * sx, 3)}, ${ldk.fmt(cov, 3)} ; ` +
`${ldk.fmt(cov, 3)}, ${ldk.fmt(sy * sy, 3)} ] det Σ = ${ldk.fmt(det, 4)}`,
{ line: 1, size: 17, fill: C.grey });
p.header(`Cov(X,Y) = ${ldk.fmt(cov, 3)} — Corr(X,Y) = ${ldk.fmt(r, 3)}` +
` (unchanged by c)`, { size: 18, fill: C.blue });
return svg;
}Definition — Multivariate Normal
\(\mathbf{X} = (X_1, \ldots, X_n)^T\) follows \(\mathcal{N}_n(\boldsymbol{\mu}, \boldsymbol{\Sigma})\) if its density is \[f_{\mathbf{X}}(\mathbf{x}) = \frac{1}{(2\pi)^{n/2}|\boldsymbol{\Sigma}|^{1/2}} \exp\left(-\tfrac{1}{2}(\mathbf{x} - \boldsymbol{\mu})^T \boldsymbol{\Sigma}^{-1} (\mathbf{x} - \boldsymbol{\mu})\right)\] with \(\boldsymbol{\mu} \in \mathbb{R}^n\) and \(\boldsymbol{\Sigma}\) positive definite.
Parameters: \(\boldsymbol{\mu} = E[\mathbf{X}]\) (mean vector) and \(\boldsymbol{\Sigma} = \text{Cov}(\mathbf{X})\) (covariance matrix).
Notation: \(\mathbf{X} \sim \mathcal{N}_n(\boldsymbol{\mu}, \boldsymbol{\Sigma})\)
The exponent is a quadratic form, so the level sets of the density are exactly the ellipses of the previous demo.
Property 4 is special
In general zero correlation says nothing about independence. Inside the multivariate normal family it says everything.
For \((X, Y) \sim \mathcal{N}_2(\boldsymbol{\mu}, \boldsymbol{\Sigma})\) with \[\boldsymbol{\mu} = \begin{pmatrix} \mu_X \\ \mu_Y \end{pmatrix}, \qquad \boldsymbol{\Sigma} = \begin{pmatrix} \sigma_X^2 & \sigma_{XY} \\ \sigma_{XY} & \sigma_Y^2 \end{pmatrix}\]
the density is \[f_{X,Y}(x,y) = \frac{1}{2\pi\sigma_X\sigma_Y\sqrt{1-\rho^2}} \exp\left(-\frac{Q}{2(1-\rho^2)}\right)\]
where \(\rho = \dfrac{\sigma_{XY}}{\sigma_X \sigma_Y}\) and \[Q = \frac{(x-\mu_X)^2}{\sigma_X^2} - \frac{2\rho(x-\mu_X)(y-\mu_Y)}{\sigma_X\sigma_Y} + \frac{(y-\mu_Y)^2}{\sigma_Y^2}\]
Given \(\mathbf{X}\) with a known distribution, find the distribution of \(\mathbf{Y} = \mathbf{g}(\mathbf{X})\), where \(\mathbf{g}: \mathbb{R}^n \to \mathbb{R}^m\).
Method 1 — CDF Method
\[F_{\mathbf{Y}}(\mathbf{y}) = P(\mathbf{g}(\mathbf{X}) \leq \mathbf{y}) = P\big(\mathbf{X} \in \{\mathbf{x} : \mathbf{g}(\mathbf{x}) \leq \mathbf{y}\}\big)\]
Method 2 — Jacobian Method (continuous)
If \(\mathbf{g}\) is one-to-one with inverse \(\mathbf{h}\), then \[f_{\mathbf{Y}}(\mathbf{y}) = f_{\mathbf{X}}\big(\mathbf{h}(\mathbf{y})\big) \cdot \big|J_{\mathbf{h}}(\mathbf{y})\big|\] where \(J_{\mathbf{h}}\) is the Jacobian determinant of \(\mathbf{h}\).
Problem. \(\mathbf{X} \sim \mathcal{N}_n(\boldsymbol{\mu}, \boldsymbol{\Sigma})\) and \(\mathbf{Y} = \mathbf{A}\mathbf{X} + \mathbf{b}\), with \(\mathbf{A}\) of size \(m \times n\) and \(\mathbf{b} \in \mathbb{R}^m\).
Solution. \[\mathbf{Y} \sim \mathcal{N}_m\big(\mathbf{A}\boldsymbol{\mu} + \mathbf{b},\ \mathbf{A}\boldsymbol{\Sigma}\mathbf{A}^T\big)\]
Special cases:
Rotate and stretch a normal cloud. It stays normal — only \(\mathbf{A}\boldsymbol{\Sigma}\mathbf{A}^T\) changes.
viewof lt_rho = Inputs.range([-0.95, 0.95], {step: 0.01, value: 0.7,
label: "Source ρ"})
viewof lt_ang = Inputs.range([0, 180], {step: 1, value: 0, label: "Rotate A by (°)"})
viewof lt_a = Inputs.range([0.3, 2.5], {step: 0.01, value: 1, label: "Stretch first axis"})
viewof lt_b = Inputs.range([0.3, 2.5], {step: 0.01, value: 1, label: "Stretch second axis"})\(\mathbf{A} = R(\theta)\,\text{diag}(a, b)\) applied to \(\mathbf{X} \sim \mathcal{N}_2(\mathbf{0}, \boldsymbol{\Sigma})\) with unit variances and correlation \(\rho\).
Set the rotation to \(45°\) with equal stretches to see a sum-and-difference transform: \(X+Y\) and \(X-Y\) become uncorrelated.
{
const C = ldk.C, r = lt_rho;
const th = lt_ang * Math.PI / 180;
// A = R(theta) diag(a, b)
const A = [[Math.cos(th) * lt_a, -Math.sin(th) * lt_b],
[Math.sin(th) * lt_a, Math.cos(th) * lt_b]];
const S = [[1, r], [r, 1]];
const mul = (M, N) => [[M[0][0]*N[0][0] + M[0][1]*N[1][0], M[0][0]*N[0][1] + M[0][1]*N[1][1]],
[M[1][0]*N[0][0] + M[1][1]*N[1][0], M[1][0]*N[0][1] + M[1][1]*N[1][1]]];
const T = m => [[m[0][0], m[1][0]], [m[0][1], m[1][1]]];
const SY = mul(mul(A, S), T(A));
const rY = SY[0][1] / Math.sqrt(SY[0][0] * SY[1][1]);
const ellOf = (M, k) => { // ellipse of the law with covariance M
const sx = Math.sqrt(M[0][0]), sy = Math.sqrt(M[1][1]);
const rr = Math.max(-1, Math.min(1, M[0][1] / (sx * sy)));
const s2 = Math.sqrt(Math.max(1 - rr * rr, 0));
const pts = [];
for (let i = 0; i <= 160; i++) {
const t = 2 * Math.PI * i / 160;
pts.push([k * sx * Math.cos(t), k * sy * (rr * Math.cos(t) + s2 * Math.sin(t))]);
}
return pts;
};
const L = 6;
const { svg, p } = ldk.single(880, 560, {
xdom: [-L, L], ydom: [-L, L], xlab: "first coordinate", ylab: "second coordinate",
margin: { t: 80 }, nx: 5, ny: 5
});
for (let k = 2.6; k > 0.1; k -= 0.25)
p.add(ldk.el("path", {
d: ellOf(SY, k).map((q, i) => (i ? "L" : "M") + p.x(q[0]).toFixed(1) + "," +
p.y(q[1]).toFixed(1)).join("") + "Z",
fill: ldk.alpha(C.blue, 0.07), stroke: "none" }));
for (const k of [1, 2])
p.path(ellOf(S, k), { stroke: ldk.alpha(C.grey, 0.8), "stroke-width": 2,
"stroke-dasharray": "8,6" });
for (const k of [1, 2])
p.path(ellOf(SY, k), { stroke: C.blue, "stroke-width": 3 });
p.legend([
{ label: "X ~ N(0, Σ)", color: C.grey, lw: 2, dash: "8,6" },
{ label: "Y = AX", color: C.blue, lw: 3 }
], { size: 15, bg: true });
p.header(`A = [ ${ldk.fmt(A[0][0], 2)}, ${ldk.fmt(A[0][1], 2)} ; ` +
`${ldk.fmt(A[1][0], 2)}, ${ldk.fmt(A[1][1], 2)} ]`,
{ line: 1, size: 16, fill: C.grey });
p.header(`AΣAᵀ = [ ${ldk.fmt(SY[0][0], 3)}, ${ldk.fmt(SY[0][1], 3)} ; ` +
`${ldk.fmt(SY[1][0], 3)}, ${ldk.fmt(SY[1][1], 3)} ]` +
` Corr = ${ldk.signed(rY, 3)}`,
{ size: 18, fill: Math.abs(rY) < 0.01 ? C.green : C.blue });
return svg;
}Given \(n\) i.i.d. random variables \(X_1, \ldots, X_n\) with CDF \(F\) and density \(f\):
Definition — Order Statistics
The order statistics \(X_{(1)} \leq X_{(2)} \leq \cdots \leq X_{(n)}\) are the sorted values of \(X_1, \ldots, X_n\).
Joint density of all order statistics: \[f_{X_{(1)}, \ldots, X_{(n)}}(x_1, \ldots, x_n) = n!\, f(x_1) \cdots f(x_n), \qquad x_1 \leq \cdots \leq x_n\]
Density of the \(k\)-th order statistic: \[f_{X_{(k)}}(x) = \frac{n!}{(k-1)!\,(n-k)!}\,[F(x)]^{k-1}\,[1-F(x)]^{n-k}\,f(x)\]
Minimum
\(X_{(1)} = \min\{X_1, \ldots, X_n\}\) \[F_{X_{(1)}}(x) = 1 - [1-F(x)]^n\] \[f_{X_{(1)}}(x) = n[1-F(x)]^{n-1}f(x)\]
Maximum
\(X_{(n)} = \max\{X_1, \ldots, X_n\}\) \[F_{X_{(n)}}(x) = [F(x)]^n\] \[f_{X_{(n)}}(x) = n[F(x)]^{n-1}f(x)\]
Range: \(R = X_{(n)} - X_{(1)}\) — the spread of the whole sample, and itself a random variable.
\(n\) draws, sorted. Each faint curve is one order statistic; the bold one is the \(k\)-th.
viewof os_dist = Inputs.select(
new Map([["Uniform(0,1)", "unif"], ["Exponential(1)", "exp"], ["N(0,1)", "norm"]]),
{label: "Parent distribution", value: "unif"}
)
viewof os_n = Inputs.range([1, 25], {step: 1, value: 8, label: "Sample size n"})
viewof os_k = Inputs.range([1, 25], {step: 1, value: 1, label: "Which one, k"})\(k = 1\) is the minimum, \(k = n\) the maximum. Exact densities — no simulation.
Note how the extremes are the most skewed, and how they pull away from the parent as \(n\) grows.
{
const C = ldk.C, n = os_n, k = Math.min(os_k, n);
const D = {
unif: { f: x => (x >= 0 && x <= 1 ? 1 : 0),
F: x => Math.max(0, Math.min(1, x)), dom: [-0.05, 1.05], lab: "Uniform(0,1)" },
exp: { f: x => (x >= 0 ? Math.exp(-x) : 0),
F: x => (x <= 0 ? 0 : 1 - Math.exp(-x)), dom: [0, 8], lab: "Exponential(1)" },
norm: { f: x => ldk.dnorm(x), F: x => ldk.pnorm(x), dom: [-4, 4], lab: "N(0,1)" }
}[os_dist];
// f_(k)(x) = n! / ((k-1)!(n-k)!) F^{k-1} (1-F)^{n-k} f
const dens = (x, kk) => {
const Fx = D.F(x), fx = D.f(x);
if (fx <= 0) return 0;
const lc = ldk.lgamma(n + 1) - ldk.lgamma(kk) - ldk.lgamma(n - kk + 1);
const a = kk - 1, b = n - kk;
if ((a > 0 && Fx <= 0) || (b > 0 && Fx >= 1)) return 0;
return Math.exp(lc + (a ? a * Math.log(Fx) : 0) +
(b ? b * Math.log1p(-Fx) : 0)) * fx;
};
const grid = ldk.seq(D.dom[0], D.dom[1], 700);
const curves = [];
for (let j = 1; j <= n; j++) curves.push(grid.map(x => [x, dens(x, j)]));
// scale to the tallest of the whole family, so the frame is fixed in k and
// every order statistic stays comparable with the others
let ymax = 0;
for (const cv of curves) for (const q of cv) if (q[1] > ymax) ymax = q[1];
ymax *= 1.1;
const { svg, p } = ldk.single(880, 540, {
xdom: D.dom, ydom: [0, ymax], xlab: "x", ylab: "density", margin: { t: 78 }
});
p.path(grid.map(x => [x, D.f(x)]),
{ stroke: C.grey, "stroke-width": 2.5, "stroke-dasharray": "8,6" });
curves.forEach((cv, j) => {
if (j + 1 === k) return;
p.path(cv, { stroke: ldk.alpha(C.blue, 0.22), "stroke-width": 1.8 });
});
p.fill(curves[k - 1], 0, { fill: ldk.alpha(C.pink, 0.2) });
p.path(curves[k - 1], { stroke: C.pink, "stroke-width": 3.5 });
// E[X_(k)] by quadrature on the plotting grid
const h = (D.dom[1] - D.dom[0]) / (grid.length - 1);
let m1 = 0, mass = 0;
for (let i = 0; i < grid.length; i++) {
const w = (i === 0 || i === grid.length - 1 ? 0.5 : 1) * h;
m1 += w * grid[i] * curves[k - 1][i][1];
mass += w * curves[k - 1][i][1];
}
p.legend([
{ label: `parent ${D.lab}`, color: C.grey, lw: 2.5, dash: "8,6" },
{ label: `X₍${k}₎ of ${n}`, color: C.pink, lw: 3.5 },
{ label: "the other order statistics", color: ldk.alpha(C.blue, 0.4), lw: 1.8 }
], { size: 14, bg: true });
p.header(k === 1 ? "k = 1: the minimum" : k === n ? "k = n: the maximum"
: `the ${k}-th of ${n}`,
{ line: 1, size: 18, fill: C.grey });
p.header(`E[X₍${k}₎] ≈ ${ldk.fmt(m1 / (mass || 1), 3)}` +
(os_dist === "unif" ? ` exact k/(n+1) = ${ldk.fmt(k / (n + 1), 3)}` : ""),
{ size: 18, fill: C.pink });
return svg;
}Key Concepts
Important Relationships
The thread
Marginals describe each coordinate on its own. Everything interesting — dependence, conditioning, prediction — lives in what the marginals leave out.
Applications in Statistics
Further Study