<!-- Two-panel layout: Gaussian copula (left) vs t-copula (right) -->
<div class="controls">
<label>Correlation ρ: <span id="rho-val">0.30</span>
<input type="range" id="rho" min="-0.95" max="0.95" step="0.05" value="0.30">
</label>
<label>t degrees of freedom ν: <span id="nu-val">3</span>
<input type="range" id="nu" min="1" max="30" step="1" value="3">
</label>
<label>Points: <span id="n-val">300</span>
<input type="range" id="nsamp" min="100" max="800" step="100" value="300">
</label>
<button id="btn-resample" type="button">Resample</button>
</div>
<div class="panels">
<div class="panel">
<div class="panel-title">Gaussian copula</div>
<canvas id="cv-gauss" width="220" height="220"></canvas>
<div class="stat" id="stat-gauss"></div>
</div>
<div class="panel">
<div class="panel-title" id="title-t">t-copula (ν = %nu%)</div>
<canvas id="cv-t" width="220" height="220"></canvas>
<div class="stat" id="stat-t"></div>
</div>
</div>
<p class="note">Red dots fall in the joint upper-right tail (both variables above the 90th percentile). The dashed box marks that region. The t-copula clusters more red points together — that is tail dependence.</p>
/* Global reset */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; font-size: 14px; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; margin-bottom: .8rem; align-items: center; }
label { display: flex; align-items: center; gap: .4rem; font-size: .85rem; }
input[type=range] { width: 90px; cursor: pointer; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
.panels { display: flex; gap: 1rem; flex-wrap: wrap; }
.panel { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
.panel-title { font-weight: 700; font-size: .88rem; color: #1d3557; }
canvas { border: 1px solid #cdd9e3; border-radius: 6px; background: #f5f8fa; display: block; }
.stat { font-size: .82rem; color: #444; min-height: 1.2em; }
.note { font-size: .82rem; color: #555; margin-top: .7rem; line-height: 1.5; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Box-Muller transform: generates a standard normal random variable
function randn() {
let u, v;
do { u = Math.random(); v = Math.random(); } while (u === 0);
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// Chi-squared variate with nu degrees of freedom (sum of nu squared normals)
function randChi2(nu) {
let s = 0;
for (let i = 0; i < nu; i++) { const z = randn(); s += z * z; }
return s;
}
// Bivariate standard normal pair with correlation rho
function bivnorm(rho) {
const z1 = randn();
const z2 = randn();
return [z1, rho * z1 + Math.sqrt(1 - rho * rho) * z2];
}
// Standard normal CDF via Abramowitz & Stegun polynomial approximation
function normCdf(x) {
const t = 1 / (1 + 0.2316419 * Math.abs(x));
const poly = t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
const d = 1 - (1 / Math.sqrt(2 * Math.PI)) * Math.exp(-0.5 * x * x) * poly;
return x >= 0 ? d : 1 - d;
}
// Stirling-series log-gamma (Lanczos approximation)
function logGamma(z) {
const c = [76.18009172947146, -86.50532032941677, 24.01409824083091, -1.231739572450155, 0.1208650973866179e-2, -0.5395239384953e-5];
let y = z, x = z, tmp = x + 5.5;
tmp -= (x + 0.5) * Math.log(tmp);
let ser = 1.000000000190015;
for (let j = 0; j < 6; j++) { y++; ser += c[j] / y; }
return -tmp + Math.log(2.5066282746310005 * ser / x);
}
// Regularized incomplete beta via Lentz continued fraction with symmetry relation
function regIncBeta(x, a, b) {
if (x <= 0) return 0;
if (x >= 1) return 1;
if (x > (a + 1) / (a + b + 2)) return 1 - regIncBeta(1 - x, b, a);
const lbeta = logGamma(a) + logGamma(b) - logGamma(a + b);
const front = Math.exp(a * Math.log(x) + b * Math.log(1 - x) - lbeta);
const fpmin = 1e-30;
let cc = 1, d = 1 - (a + b) * x / (a + 1);
if (Math.abs(d) < fpmin) d = fpmin;
d = 1 / d; let h = d;
for (let m = 1; m <= 200; m++) {
let aa = m * (b - m) * x / ((a + 2 * m - 1) * (a + 2 * m));
d = 1 + aa * d; if (Math.abs(d) < fpmin) d = fpmin; d = 1 / d;
cc = 1 + aa / cc; if (Math.abs(cc) < fpmin) cc = fpmin; h *= d * cc;
aa = -(a + m) * (a + b + m) * x / ((a + 2 * m) * (a + 2 * m + 1));
d = 1 + aa * d; if (Math.abs(d) < fpmin) d = fpmin; d = 1 / d;
cc = 1 + aa / cc; if (Math.abs(cc) < fpmin) cc = fpmin;
const del = d * cc; h *= del;
if (Math.abs(del - 1) < 1e-10) break;
}
return front * h / a;
}
// Student t-CDF using the regularized incomplete beta: I_x(nu/2, 0.5)
function tCdf(x, nu) {
const z = nu / (nu + x * x);
const ib = regIncBeta(z, nu / 2, 0.5);
return x >= 0 ? 1 - ib / 2 : ib / 2;
}
// Sample n points from a Gaussian copula with given correlation
function sampleGaussianCopula(n, rho) {
return Array.from({ length: n }, () => {
const [z1, z2] = bivnorm(rho);
return [normCdf(z1), normCdf(z2)];
});
}
// Sample n points from a t-copula: use bivariate normal / chi2 scale, then apply t-CDF
function sampleTCopula(n, rho, nu) {
return Array.from({ length: n }, () => {
const [z1, z2] = bivnorm(rho);
const w = Math.sqrt(randChi2(nu) / nu);
const t1 = z1 / w, t2 = z2 / w;
return [tCdf(t1, nu), tCdf(t2, nu)];
});
}
// Count points in the upper-right joint tail (both > q)
function tailCount(pts, q) {
return pts.filter(([u, v]) => u > q && v > q).length;
}
function draw(cvId, pts, rho, label, statId, nu) {
const cv = document.getElementById(cvId);
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
const pad = 18;
ctx.clearRect(0, 0, W, H);
// Draw axis lines
ctx.strokeStyle = '#cdd9e3'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(pad, pad); ctx.lineTo(pad, H - pad); ctx.lineTo(W - pad, H - pad); ctx.stroke();
// Highlight the upper-right tail region (both > 90th pct)
const q = 0.90;
const tx = pad + (W - 2 * pad) * q, ty = H - pad - (H - 2 * pad) * q;
ctx.strokeStyle = '#e6394655'; ctx.setLineDash([3, 3]);
ctx.strokeRect(tx, pad, W - pad - tx, ty - pad);
ctx.setLineDash([]);
// Draw each sampled point; highlight tail points in red
pts.forEach(([u, v]) => {
const inTail = u > q && v > q;
const x = pad + (W - 2 * pad) * u;
const y = H - pad - (H - 2 * pad) * v;
ctx.beginPath(); ctx.arc(x, y, inTail ? 3 : 1.6, 0, 2 * Math.PI);
ctx.fillStyle = inTail ? '#e63946' : '#1d355755';
ctx.fill();
});
const cnt = tailCount(pts, q);
const stat = document.getElementById(statId);
stat.textContent = i18nFmt('%cnt% joint tail points (%pct%%)', { cnt: cnt, pct: (cnt / pts.length * 100).toFixed(1) });
}
function run() {
const rho = parseFloat(document.getElementById('rho').value);
const nu = parseInt(document.getElementById('nu').value);
const n = parseInt(document.getElementById('nsamp').value);
const gPts = sampleGaussianCopula(n, rho);
const tPts = sampleTCopula(n, rho, nu);
draw('cv-gauss', gPts, rho, 'Gaussian copula', 'stat-gauss', null);
draw('cv-t', tPts, rho, 't-copula (ν = %nu%)', 'stat-t', nu);
document.getElementById('rho-val').textContent = rho.toFixed(2);
document.getElementById('nu-val').textContent = nu;
document.getElementById('title-t').textContent = i18nFmt('t-copula (ν = %nu%)', { nu: nu });
document.getElementById('n-val').textContent = n;
}
document.getElementById('btn-resample').addEventListener('click', run);
document.getElementById('rho').addEventListener('input', run);
document.getElementById('nu').addEventListener('input', run);
document.getElementById('nsamp').addEventListener('input', run);
run();
<!-- Two-panel layout: Gaussian copula (left) vs t-copula (right) -->
<div class="controls">
<label>Correlation ρ: <span id="rho-val">0.30</span>
<input type="range" id="rho" min="-0.95" max="0.95" step="0.05" value="0.30">
</label>
<label>t degrees of freedom ν: <span id="nu-val">3</span>
<input type="range" id="nu" min="1" max="30" step="1" value="3">
</label>
<label>Points: <span id="n-val">300</span>
<input type="range" id="nsamp" min="100" max="800" step="100" value="300">
</label>
<button id="btn-resample" type="button">Resample</button>
</div>
<div class="panels">
<div class="panel">
<div class="panel-title">Gaussian copula</div>
<canvas id="cv-gauss" width="220" height="220"></canvas>
<div class="stat" id="stat-gauss"></div>
</div>
<div class="panel">
<div class="panel-title" id="title-t">t-copula (ν = %nu%)</div>
<canvas id="cv-t" width="220" height="220"></canvas>
<div class="stat" id="stat-t"></div>
</div>
</div>
<p class="note">Red dots fall in the joint upper-right tail (both variables above the 90th percentile). The dashed box marks that region. The t-copula clusters more red points together — that is tail dependence.</p>
/* Global reset */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; font-size: 14px; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; margin-bottom: .8rem; align-items: center; }
label { display: flex; align-items: center; gap: .4rem; font-size: .85rem; }
input[type=range] { width: 90px; cursor: pointer; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
.panels { display: flex; gap: 1rem; flex-wrap: wrap; }
.panel { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
.panel-title { font-weight: 700; font-size: .88rem; color: #1d3557; }
canvas { border: 1px solid #cdd9e3; border-radius: 6px; background: #f5f8fa; display: block; }
.stat { font-size: .82rem; color: #444; min-height: 1.2em; }
.note { font-size: .82rem; color: #555; margin-top: .7rem; line-height: 1.5; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Box-Muller transform: generates a standard normal random variable
function randn() {
let u, v;
do { u = Math.random(); v = Math.random(); } while (u === 0);
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// Chi-squared variate with nu degrees of freedom (sum of nu squared normals)
function randChi2(nu) {
let s = 0;
for (let i = 0; i < nu; i++) { const z = randn(); s += z * z; }
return s;
}
// Bivariate standard normal pair with correlation rho
function bivnorm(rho) {
const z1 = randn();
const z2 = randn();
return [z1, rho * z1 + Math.sqrt(1 - rho * rho) * z2];
}
// Standard normal CDF via Abramowitz & Stegun polynomial approximation
function normCdf(x) {
const t = 1 / (1 + 0.2316419 * Math.abs(x));
const poly = t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
const d = 1 - (1 / Math.sqrt(2 * Math.PI)) * Math.exp(-0.5 * x * x) * poly;
return x >= 0 ? d : 1 - d;
}
// Stirling-series log-gamma (Lanczos approximation)
function logGamma(z) {
const c = [76.18009172947146, -86.50532032941677, 24.01409824083091, -1.231739572450155, 0.1208650973866179e-2, -0.5395239384953e-5];
let y = z, x = z, tmp = x + 5.5;
tmp -= (x + 0.5) * Math.log(tmp);
let ser = 1.000000000190015;
for (let j = 0; j < 6; j++) { y++; ser += c[j] / y; }
return -tmp + Math.log(2.5066282746310005 * ser / x);
}
// Regularized incomplete beta via Lentz continued fraction with symmetry relation
function regIncBeta(x, a, b) {
if (x <= 0) return 0;
if (x >= 1) return 1;
if (x > (a + 1) / (a + b + 2)) return 1 - regIncBeta(1 - x, b, a);
const lbeta = logGamma(a) + logGamma(b) - logGamma(a + b);
const front = Math.exp(a * Math.log(x) + b * Math.log(1 - x) - lbeta);
const fpmin = 1e-30;
let cc = 1, d = 1 - (a + b) * x / (a + 1);
if (Math.abs(d) < fpmin) d = fpmin;
d = 1 / d; let h = d;
for (let m = 1; m <= 200; m++) {
let aa = m * (b - m) * x / ((a + 2 * m - 1) * (a + 2 * m));
d = 1 + aa * d; if (Math.abs(d) < fpmin) d = fpmin; d = 1 / d;
cc = 1 + aa / cc; if (Math.abs(cc) < fpmin) cc = fpmin; h *= d * cc;
aa = -(a + m) * (a + b + m) * x / ((a + 2 * m) * (a + 2 * m + 1));
d = 1 + aa * d; if (Math.abs(d) < fpmin) d = fpmin; d = 1 / d;
cc = 1 + aa / cc; if (Math.abs(cc) < fpmin) cc = fpmin;
const del = d * cc; h *= del;
if (Math.abs(del - 1) < 1e-10) break;
}
return front * h / a;
}
// Student t-CDF using the regularized incomplete beta: I_x(nu/2, 0.5)
function tCdf(x, nu) {
const z = nu / (nu + x * x);
const ib = regIncBeta(z, nu / 2, 0.5);
return x >= 0 ? 1 - ib / 2 : ib / 2;
}
// Sample n points from a Gaussian copula with given correlation
function sampleGaussianCopula(n, rho) {
return Array.from({ length: n }, () => {
const [z1, z2] = bivnorm(rho);
return [normCdf(z1), normCdf(z2)];
});
}
// Sample n points from a t-copula: use bivariate normal / chi2 scale, then apply t-CDF
function sampleTCopula(n, rho, nu) {
return Array.from({ length: n }, () => {
const [z1, z2] = bivnorm(rho);
const w = Math.sqrt(randChi2(nu) / nu);
const t1 = z1 / w, t2 = z2 / w;
return [tCdf(t1, nu), tCdf(t2, nu)];
});
}
// Count points in the upper-right joint tail (both > q)
function tailCount(pts, q) {
return pts.filter(([u, v]) => u > q && v > q).length;
}
function draw(cvId, pts, rho, label, statId, nu) {
const cv = document.getElementById(cvId);
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
const pad = 18;
ctx.clearRect(0, 0, W, H);
// Draw axis lines
ctx.strokeStyle = '#cdd9e3'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(pad, pad); ctx.lineTo(pad, H - pad); ctx.lineTo(W - pad, H - pad); ctx.stroke();
// Highlight the upper-right tail region (both > 90th pct)
const q = 0.90;
const tx = pad + (W - 2 * pad) * q, ty = H - pad - (H - 2 * pad) * q;
ctx.strokeStyle = '#e6394655'; ctx.setLineDash([3, 3]);
ctx.strokeRect(tx, pad, W - pad - tx, ty - pad);
ctx.setLineDash([]);
// Draw each sampled point; highlight tail points in red
pts.forEach(([u, v]) => {
const inTail = u > q && v > q;
const x = pad + (W - 2 * pad) * u;
const y = H - pad - (H - 2 * pad) * v;
ctx.beginPath(); ctx.arc(x, y, inTail ? 3 : 1.6, 0, 2 * Math.PI);
ctx.fillStyle = inTail ? '#e63946' : '#1d355755';
ctx.fill();
});
const cnt = tailCount(pts, q);
const stat = document.getElementById(statId);
stat.textContent = i18nFmt('%cnt% joint tail points (%pct%%)', { cnt: cnt, pct: (cnt / pts.length * 100).toFixed(1) });
}
function run() {
const rho = parseFloat(document.getElementById('rho').value);
const nu = parseInt(document.getElementById('nu').value);
const n = parseInt(document.getElementById('nsamp').value);
const gPts = sampleGaussianCopula(n, rho);
const tPts = sampleTCopula(n, rho, nu);
draw('cv-gauss', gPts, rho, 'Gaussian copula', 'stat-gauss', null);
draw('cv-t', tPts, rho, 't-copula (ν = %nu%)', 'stat-t', nu);
document.getElementById('rho-val').textContent = rho.toFixed(2);
document.getElementById('nu-val').textContent = nu;
document.getElementById('title-t').textContent = i18nFmt('t-copula (ν = %nu%)', { nu: nu });
document.getElementById('n-val').textContent = n;
}
document.getElementById('btn-resample').addEventListener('click', run);
document.getElementById('rho').addEventListener('input', run);
document.getElementById('nu').addEventListener('input', run);
document.getElementById('nsamp').addEventListener('input', run);
run();
Comments
Loading comments...