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);
}
// ---------------------------------------------------------------- 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, dchisq, pchisq, qchisq, dt, pt, qt, invert,
seq, cumsum, sum, mean, sd, quantileSorted, hist, rng, sampleWeighted,
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\).
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)\]
Generally: Copulas
Sklar’s Theorem. Any joint distribution can be written as \[F_{X,Y}(x,y) = C\big(F_X(x), F_Y(y)\big)\] where the copula \(C\) carries the entire dependence structure — separately from the marginals.
Types of dependence:
Popular alternative measures:
Every panel below has exactly the same marginals. Only the copula changes.
Both coordinates are pushed through their own rank transform, so each margin is exactly the same set of normal scores in every panel. Anything you see change is dependence, not marginals.
Watch the U-shape: Pearson, Spearman and Kendall are all near zero, yet \(Y\) is almost a function of \(X\).
{
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 { const w = Math.sqrt(u.exp()); a = z1 / w; b = (0.6 * z1 + 0.8 * z2) / w; }
rx[i] = a; ry[i] = b;
}
// rank -> normal scores: identical marginals for every structure
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] = ldk.qnorm((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 L = 3.4;
const svg = ldk.chart(900, 560);
// ---- main scatter
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 i = 0; i < N; i++)
pm.dot(x[i], y[i], 3.4, { fill: ldk.alpha(C.blue, 0.42), stroke: "none" });
// ---- marginal histograms, identical by construction
const bins = 34;
const hx = ldk.hist(x, -L, L, bins), hy = ldk.hist(y, -L, L, bins);
let dm = 0;
for (let i = 0; i < bins; i++) dm = Math.max(dm, hx.dens[i], hy.dens[i]);
dm *= 1.15;
const pt = ldk.panel(svg, {
left: 0, top: 0, w: 620, h: 160, xdom: [-L, L], ydom: [0, dm],
xticks: [], ylab: "f(x)", margin: { l: 76, r: 14, t: 62, b: 6 }, ny: 2
});
for (let i = 0; i < bins; i++)
pt.bar(-L + i * hx.w, -L + (i + 1) * hx.w, 0, hx.dens[i],
{ fill: ldk.alpha(C.grey, 0.4), stroke: "white", "stroke-width": 0.8 });
pt.header(`Pearson r = ${ldk.signed(rP, 3)} Spearman ρ = ${ldk.signed(rS, 3)}` +
` Kendall τ = ${ldk.signed(tau, 3)}`,
{ line: 1, size: 18,
fill: cp_kind === "ushape" ? C.pink : C.ink });
pt.header("marginal of X — the same in every structure", { size: 16, fill: C.grey });
const pr = ldk.panel(svg, {
left: 620, top: 160, w: 280, h: 400, xdom: [0, dm], ydom: [-L, L],
yticks: [], xlab: "f(y)", margin: { l: 12, r: 40, t: 6, b: 56 }, nx: 2
});
for (let i = 0; i < bins; i++)
pr.bar(0, hy.dens[i], -L + i * hy.w, -L + (i + 1) * hy.w,
{ fill: ldk.alpha(C.grey, 0.4), stroke: "white", "stroke-width": 0.8 });
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