| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"/> |
| <meta name="viewport" content="width=device-width,initial-scale=1"/> |
| <title>Continuous Distributions Lab (Interactive)</title> |
| <style> |
| :root{--bg:#0f172a;--card:#111827;--ink:#e5e7eb;--mut:#94a3b8;--accent:#60a5fa;--ok:#10b981;--warn:#f59e0b} |
| *{box-sizing:border-box} |
| body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:linear-gradient(180deg,#0b132b,#0f172a);color:var(--ink);padding:24px} |
| h1{margin:0 0 4px 0;font-size:1.35rem} |
| .tag{color:#cbd5e1;margin-bottom:14px} |
| .wrap{display:grid;grid-template-columns:360px 1fr;gap:16px} |
| .card{background:rgba(17,24,39,.75);border:1px solid #1f2937;border-radius:16px;padding:14px;box-shadow:0 16px 30px rgba(0,0,0,.25)} |
| label{display:block;margin:8px 0 4px;color:#cbd5e1;font-size:.9rem} |
| input[type="number"],select{width:100%} |
| .row{display:grid;grid-template-columns:1fr 1fr;gap:10px} |
| .buttons{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px} |
| button{background:#2563eb;border:0;color:white;font-weight:700;border-radius:999px;padding:10px 14px;cursor:pointer} |
| button.secondary{background:#334155} |
| button.warn{background:#b45309} |
| button:active{transform:scale(.98)} |
| .pill{border:1px solid #334155;border-radius:999px;padding:.2rem .6rem;color:#cbd5e1;font-size:.8rem} |
| .stat{display:flex;justify-content:space-between;gap:8px;font-variant-numeric:tabular-nums} |
| canvas{width:100%;height:360px;border-radius:12px;background:#0b1220;border:1px solid #1f2937} |
| .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace} |
| .small{font-size:.85rem;color:var(--mut)} |
| .grid2{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:12px} |
| </style> |
| </head> |
| <body> |
| <h1>Continuous Distributions Lab</h1> |
| <div class="tag">Pick a distribution, set parameters, simulate samples, and compare the histogram to the theoretical PDF.</div> |
|
|
| <div class="wrap"> |
| |
| <div class="card"> |
| <label>Distribution</label> |
| <select id="dist"> |
| <option value="normal">Normal (ΞΌ, Ο)</option> |
| <option value="uniform">Uniform (a, b)</option> |
| <option value="exponential">Exponential (Ξ»)</option> |
| <option value="lognormal">Lognormal (ΞΌ, Ο)</option> |
| <option value="triangular">Triangular (a, c, b)</option> |
| <option value="logistic">Logistic (ΞΌ, s)</option> |
| <option value="weibull">Weibull (k, Ξ»)</option> |
| <option value="t">Studentβs t (Ξ½)</option> |
| <option value="chisq">Chi-square (k)</option> |
| <option value="cauchy">Cauchy (xβ, Ξ³)</option> |
| </select> |
|
|
| <div id="paramBox"></div> |
|
|
| <div class="row"> |
| <div> |
| <label>Samples per click</label> |
| <input id="n" type="number" min="10" max="200000" value="2000"/> |
| </div> |
| <div> |
| <label>Bins</label> |
| <input id="bins" type="number" min="10" max="120" value="40"/> |
| </div> |
| </div> |
|
|
| <div class="buttons"> |
| <button id="sample">Sample</button> |
| <button class="secondary" id="more">+10k more</button> |
| <button class="warn" id="reset">Reset</button> |
| </div> |
|
|
| <div style="margin-top:10px"> |
| <div class="stat"><span>Total samples</span><span id="count" class="mono">0</span></div> |
| <div class="stat"><span>Range plotted</span><span id="range" class="mono"></span></div> |
| </div> |
|
|
| <hr style="border:0;border-top:1px solid #223047;margin:12px 0"> |
| <div class="grid2"> |
| <div class="small"> |
| <div><strong>Sample stats</strong></div> |
| <div class="stat"><span>Mean</span><span id="sMean" class="mono">β</span></div> |
| <div class="stat"><span>Variance</span><span id="sVar" class="mono">β</span></div> |
| <div class="stat"><span>Std. dev</span><span id="sSd" class="mono">β</span></div> |
| <div class="stat"><span>KS distance</span><span id="ks" class="mono">β</span></div> |
| </div> |
| <div class="small"> |
| <div><strong>Theoretical (when defined)</strong></div> |
| <div class="stat"><span>Mean</span><span id="tMean" class="mono">β</span></div> |
| <div class="stat"><span>Variance</span><span id="tVar" class="mono">β</span></div> |
| <div class="stat"><span>Std. dev</span><span id="tSd" class="mono">β</span></div> |
| <div class="stat"><span class="pill">Blue = histogram, Amber = PDF</span><span></span></div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="card"> |
| <canvas id="plot" width="1200" height="540"></canvas> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const $ = id => document.getElementById(id); |
| const distSel = $('dist'), nEl=$('n'), binsEl=$('bins'); |
| const paramBox = $('paramBox'), canvas=$('plot'), ctx=canvas.getContext('2d'); |
| const countEl=$('count'), rangeEl=$('range'); |
| const sMean=$('sMean'), sVar=$('sVar'), sSd=$('sSd'), ksEl=$('ks'); |
| const tMean=$('tMean'), tVar=$('tVar'), tSd=$('tSd'); |
| |
| let samples=[], params={}; |
| function clamp(x,a,b){return Math.max(a,Math.min(b,x))} |
| function mean(a){return a.reduce((s,x)=>s+x,0)/a.length} |
| function variance(a,m){const mu=m??mean(a);return a.reduce((s,x)=>s+(x-mu)*(x-mu),0)/a.length} |
| function std(a,m){return Math.sqrt(variance(a,m))} |
| function linspace(a,b,n){const arr=[];const step=(b-a)/(n-1);for(let i=0;i<n;i++)arr.push(a+i*step);return arr} |
| |
| |
| const UIs = { |
| normal(){ return [ |
| ['ΞΌ','mu',0,-1e9,1e9,0.0], |
| ['Ο (>0)','sigma',1,1e-6,1e6,0.1], |
| ];}, |
| uniform(){ return [ |
| ['a','a',0,-1e9,1e9,0.0], |
| ['b (>a)','b',1,-1e9,1e9,1.0], |
| ];}, |
| exponential(){ return [ |
| ['Ξ» (>0)','lambda',1e0,1e-6,1e6,0.1], |
| ];}, |
| lognormal(){ return [ |
| ['ΞΌ (log-space)','mu',0,-1e6,1e6,0.1], |
| ['Ο (>0)','sigma',0.5,1e-6,1e6,0.1], |
| ];}, |
| triangular(){ return [ |
| ['a (min)','a',0,-1e6,1e6,0.1], |
| ['c (mode)','c',0.5,-1e6,1e6,0.1], |
| ['b (max)','b',1,-1e6,1e6,0.1], |
| ];}, |
| logistic(){ return [ |
| ['ΞΌ','mu',0,-1e6,1e6,0.1], |
| ['s (>0)','s',1,1e-6,1e6,0.1], |
| ];}, |
| weibull(){ return [ |
| ['k (>0)','k',1.5,1e-6,1e6,0.1], |
| ['Ξ» (>0)','lambda',1,1e-6,1e6,0.1], |
| ];}, |
| t(){ return [ |
| ['Ξ½ (>0)','nu',5,1e-6,1e6,1], |
| ];}, |
| chisq(){ return [ |
| ['k (dof >0)','k',4,1e-6,1e6,1], |
| ];}, |
| cauchy(){ return [ |
| ['xβ','x0',0,-1e6,1e6,0.1], |
| ['Ξ³ (>0)','gamma',1,1e-6,1e6,0.1], |
| ];} |
| } |
| |
| function renderParams(){ |
| paramBox.innerHTML=''; |
| const spec = UIs[distSel.value](); |
| spec.forEach(([label, key, def, min, max, step])=>{ |
| const id='p_'+key; |
| const wrapper=document.createElement('div'); |
| wrapper.innerHTML = `<label for="${id}">${label}</label> |
| <input id="${id}" type="number" step="${step}" value="${def}" min="${min}" max="${max}"/>`; |
| paramBox.appendChild(wrapper); |
| params[key]=def; |
| $(id).addEventListener('input', e=>{ |
| const v = Number(e.target.value); |
| params[key]=v; |
| if(distSel.value==='triangular'){ |
| |
| const a=Number($('p_a').value), c=Number($('p_c').value), b=Number($('p_b').value); |
| if(a>b){$('p_b').value=a+1; params.b=a+1;} |
| $('p_c').value = clamp(c, Math.min(a,b), Math.max(a,b)); |
| params.c=Number($('p_c').value); |
| } |
| redraw(); |
| }); |
| }); |
| } |
| distSel.addEventListener('change', ()=>{samples=[];renderParams();redraw();}); |
| renderParams(); |
| |
| |
| function U(){ return Math.random(); } |
| function randn(){ |
| let u=0,v=0; while(u===0)u=U(); while(v===0)v=U(); |
| return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v); |
| } |
| function chiSquare(k){ |
| if (Number.isInteger(k) && k>0){ |
| let s=0; for(let i=0;i<k;i++){const z=randn(); s+=z*z;} return s; |
| } |
| |
| const a = k/2; |
| if(a<1){ |
| |
| const u = U(); |
| return gammaSample(a+1) * Math.pow(u,1/a) * 2; |
| } |
| function gammaSample(shape){ |
| const d = shape - 1/3, c = 1/Math.sqrt(9*d); |
| while(true){ |
| let x = randn(); |
| let v = Math.pow(1 + c*x, 3); |
| if(v<=0) continue; |
| let u = U(); |
| if(u < 1 - 0.0331*Math.pow(x,4)) return d*v; |
| if(Math.log(u) < 0.5*x*x + d*(1 - v + Math.log(v))) return d*v; |
| } |
| } |
| return 2*gammaSample(a); |
| } |
| |
| const RNG = { |
| normal: ({mu,sigma}) => mu + sigma*randn(), |
| uniform: ({a,b}) => a + (b-a)*U(), |
| exponential: ({lambda}) => -Math.log(1-U())/lambda, |
| lognormal: ({mu,sigma}) => Math.exp(mu + sigma*randn()), |
| triangular: ({a,c,b}) => { |
| const u=U(); const F=(c-a)/(b-a); |
| if(u<F) return a + Math.sqrt(u*(b-a)*(c-a)); |
| return b - Math.sqrt((1-u)*(b-a)*(b-c)); |
| }, |
| logistic: ({mu,s}) => mu + s*Math.log(U()/(1-U())), |
| weibull: ({k,lambda}) => lambda*Math.pow(-Math.log(1-U()), 1/k), |
| t: ({nu}) => { |
| const z = randn(); |
| const v = chiSquare(Math.round(nu)); |
| return z / Math.sqrt(v/nu); |
| }, |
| chisq: ({k}) => chiSquare(Math.round(k)), |
| cauchy: ({x0,gamma}) => x0 + gamma*Math.tan(Math.PI*(U()-0.5)), |
| }; |
| |
| |
| function pdf_normal(x,{mu,sigma}){return (1/(sigma*Math.sqrt(2*Math.PI)))*Math.exp(-0.5*Math.pow((x-mu)/sigma,2))} |
| function pdf_uniform(x,{a,b}){return (x<a||x>b)?0:1/(b-a)} |
| function pdf_exponential(x,{lambda}){return x<0?0:lambda*Math.exp(-lambda*x)} |
| function pdf_lognormal(x,{mu,sigma}){return x<=0?0:(1/(x*sigma*Math.sqrt(2*Math.PI)))*Math.exp(-Math.pow(Math.log(x)-mu,2)/(2*sigma*sigma))} |
| function pdf_triangular(x,{a,c,b}){ |
| if(x<a||x>b) return 0; |
| if(x<=c) return 2*(x-a)/((b-a)*(c-a)); |
| return 2*(b-x)/((b-a)*(b-c)); |
| } |
| function pdf_logistic(x,{mu,s}){const t=Math.exp(-(x-mu)/s); const d=(1+t); return (1/s)*(t/(d*d))} |
| function pdf_weibull(x,{k,lambda}){return x<0?0:(k/Math.pow(lambda,k))*Math.pow(x,k-1)*Math.exp(-Math.pow(x/lambda,k))} |
| function pdf_t(x,{nu}){const g=Math.gamma?Math.gamma:gammaJS; |
| const c = g((nu+1)/2)/(Math.sqrt(nu*Math.PI)*g(nu/2)); |
| return c*Math.pow(1+x*x/nu, -(nu+1)/2); |
| } |
| function pdf_chisq(x,{k}){if(x<0) return 0; const g=Math.gamma?Math.gamma:gammaJS; return Math.pow(x,k/2-1)*Math.exp(-x/2)/(Math.pow(2,k/2)*g(k/2))} |
| function pdf_cauchy(x,{x0,gamma}){return (1/Math.PI)*(gamma/( (x-x0)*(x-x0)+gamma*gamma ))} |
| |
| |
| function gammaJS(z){ |
| const p=[676.5203681218851,-1259.1392167224028,771.32342877765313,-176.61502916214059,12.507343278686905,-0.13857109526572012,9.9843695780195716e-6,1.5056327351493116e-7]; |
| if(z<0.5) return Math.PI/ (Math.sin(Math.PI*z)*gammaJS(1-z)); |
| z-=1; let x=0.99999999999980993; |
| for(let i=0;i<p.length;i++) x+=p[i]/(z+i+1); |
| const t=z+p.length-0.5; |
| return Math.sqrt(2*Math.PI)*Math.pow(t,z+0.5)*Math.exp(-t)*x; |
| } |
| |
| const PDF = { |
| normal: pdf_normal, uniform: pdf_uniform, exponential: pdf_exponential, lognormal: pdf_lognormal, |
| triangular: pdf_triangular, logistic: pdf_logistic, weibull: pdf_weibull, t: pdf_t, |
| chisq: pdf_chisq, cauchy: pdf_cauchy |
| } |
| |
| |
| function theory(dist,p){ |
| switch(dist){ |
| case 'normal': return {mean:p.mu, var:p.sigma*p.sigma}; |
| case 'uniform': return {mean:(p.a+p.b)/2, var:Math.pow(p.b-p.a,2)/12}; |
| case 'exponential': return {mean:1/p.lambda, var:1/(p.lambda*p.lambda)}; |
| case 'lognormal': { |
| const m=Math.exp(p.mu + p.sigma*p.sigma/2); |
| const v=(Math.exp(p.sigma*p.sigma)-1)*Math.exp(2*p.mu + p.sigma*p.sigma); |
| return {mean:m,var:v}; |
| } |
| case 'triangular': { |
| const mean=(p.a+p.b+p.c)/3; |
| const v=(p.a*p.a + p.b*p.b + p.c*p.c - p.a*p.b - p.a*p.c - p.b*p.c)/18; |
| return {mean:mean,var:v}; |
| } |
| case 'logistic': return {mean:p.mu, var:(Math.PI*Math.PI/3)*p.s*p.s}; |
| case 'weibull': { |
| const g=Math.gamma?Math.gamma:gammaJS; |
| const m=p.lambda*g(1+1/p.k); |
| const v=p.lambda*p.lambda*( g(1+2/p.k) - Math.pow(g(1+1/p.k),2) ); |
| return {mean:m,var:v}; |
| } |
| case 't': { |
| const nu=p.nu; |
| if(nu>1 && nu<=2) return {mean:0,var:Infinity}; |
| if(nu<=1) return {mean:undefined,var:undefined}; |
| return {mean:0,var: nu/(nu-2)}; |
| } |
| case 'chisq': return {mean:p.k, var:2*p.k}; |
| case 'cauchy': return {mean:undefined, var:undefined}; |
| } |
| } |
| |
| |
| function sampleOnce(dist,p){ return RNG[dist](p); } |
| function sampleN(m){ |
| const d=distSel.value; |
| for(let i=0;i<m;i++) samples.push(sampleOnce(d,params)); |
| } |
| |
| function computeKS(){ |
| if(samples.length===0) return NaN; |
| const d=distSel.value, p=params; |
| const s=[...samples].sort((a,b)=>a-b); |
| let D=0; |
| for(let i=0;i<s.length;i++){ |
| const x=s[i]; |
| const F_emp=(i+1)/s.length; |
| const F_th=cdf(d,p,x); |
| D=Math.max(D,Math.abs(F_emp-F_th)); |
| } |
| return D; |
| } |
| |
| |
| function erf(x){ |
| const sign = x<0?-1:1; x=Math.abs(x); |
| const a1=0.254829592,a2=-0.284496736,a3=1.421413741,a4=-1.453152027,a5=1.061405429,p=0.3275911; |
| const t=1/(1+p*x); const y=1-((((a5*t+a4)*t+a3)*t+a2)*t+a1)*t*Math.exp(-x*x); |
| return sign*y; |
| } |
| function cdf(dist,p,x){ |
| switch(dist){ |
| case 'normal': return 0.5*(1+erf((x-p.mu)/(p.sigma*Math.SQRT2))); |
| case 'uniform': return x<=p.a?0:x>=p.b?1:(x-p.a)/(p.b-p.a); |
| case 'exponential': return x<0?0:1-Math.exp(-p.lambda*x); |
| case 'lognormal': return x<=0?0:0.5*(1+erf((Math.log(x)-p.mu)/(p.sigma*Math.SQRT2))); |
| case 'triangular': { |
| const {a,c,b}=p; |
| if(x<=a) return 0; if(x>=b) return 1; |
| if(x<=c) return Math.pow(x-a,2)/((b-a)*(c-a)); |
| return 1 - Math.pow(b-x,2)/((b-a)*(b-c)); |
| } |
| case 'logistic': { const t=1/(1+Math.exp(-(x-p.mu)/p.s)); return t; } |
| case 'weibull': return x<0?0:1-Math.exp(-Math.pow(x/p.lambda,p.k)); |
| case 't': { |
| const xs=linspace(-10,10,800); let A=0,prev=xs[0]; |
| for(const y of xs){ if(y>x) break; A+=PDF.t(y,p)*(y-prev); prev=y;} |
| return clamp(A+0.5,0,1); |
| } |
| case 'chisq': { |
| if(x<=0) return 0; |
| |
| const k=p.k/2, t=x/2; |
| return lowerGammaReg(k,t); |
| } |
| case 'cauchy': return (1/Math.PI)*Math.atan((x-p.x0)/p.gamma)+0.5; |
| } |
| } |
| function lowerGammaReg(s,x){ |
| if(x<=0) return 0; |
| let sum=1/s, term=sum; |
| for(let n=1;n<100;n++){ |
| term *= x/(s+n); |
| sum += term; |
| if(term<1e-12) break; |
| } |
| return Math.exp(-x + s*Math.log(x) - Math.log(gammaJS(s))) * sum; |
| } |
| |
| |
| function redraw(){ |
| |
| const th=theory(distSel.value,params)||{}; |
| tMean.textContent = th.mean===undefined?'β':Number.isFinite(th.mean)?th.mean.toFixed(4):'β'; |
| tVar.textContent = th.var===undefined?'β':Number.isFinite(th.var)?th.var.toFixed(4):'β'; |
| tSd.textContent = th.var===undefined?'β':Number.isFinite(th.var)?Math.sqrt(th.var).toFixed(4):'β'; |
| |
| |
| let xmin, xmax; |
| if(samples.length>0){ |
| xmin = Math.min(...samples), xmax=Math.max(...samples); |
| } else { |
| const d=distSel.value, p=params; |
| if(d==='uniform'||d==='triangular'){ xmin=p.a; xmax=p.b; } |
| else if(d==='exponential'||d==='chisq'||d==='weibull'||d==='lognormal'){ xmin=0; xmax=5; } |
| else { xmin=-5; xmax=5; } |
| } |
| if(xmin===xmax){ xmin-=1; xmax+=1; } |
| const pad = (xmax-xmin)*0.08; xmin-=pad; xmax+=pad; |
| |
| rangeEl.textContent = `${xmin.toFixed(3)} β¦ ${xmax.toFixed(3)}`; |
| |
| |
| const bins = Math.max(5, Math.min(120, Number(binsEl.value))); |
| const w = (xmax-xmin)/bins; |
| const counts = new Array(bins).fill(0); |
| for(const x of samples){ |
| const j = Math.floor((x - xmin)/w); |
| if(j>=0 && j<bins) counts[j]++; |
| } |
| const maxCount = Math.max(1, ...counts); |
| |
| |
| ctx.clearRect(0,0,canvas.width,canvas.height); |
| const W=canvas.width, H=canvas.height, L=50, B=40, R=20, T=14; |
| |
| ctx.strokeStyle='#2b3957'; ctx.lineWidth=1; |
| ctx.beginPath(); ctx.moveTo(L,T); ctx.lineTo(L,H-B); ctx.lineTo(W-R,H-B); ctx.stroke(); |
| |
| |
| const barW=(W-L-R)/bins; |
| for(let i=0;i<bins;i++){ |
| const h = (H-B-T)*(counts[i]/maxCount); |
| ctx.fillStyle='rgba(96,165,250,.9)'; |
| ctx.fillRect(L+i*barW, H-B-h, barW-1, h); |
| } |
| |
| |
| const d=distSel.value, p=params, pdf=PDF[d]; |
| if(pdf){ |
| const xs = linspace(xmin,xmax,600); |
| const ys = xs.map(x=>pdf(x,p)); |
| |
| const area = ys.reduce((s,y,i)=> s + y*( (i?xs[i]-xs[i-1]:0) ), 0); |
| const scale = (samples.length) / (area||1); |
| const toY = y=> (H-B) - ( (y*scale)/maxCount )*(H-B-T); |
| ctx.beginPath(); ctx.lineWidth=2; ctx.strokeStyle='#f59e0b'; |
| xs.forEach((x,i)=>{ |
| const px = L + ( (x - xmin)/(xmax-xmin) )*(W-L-R); |
| const py = toY(ys[i]); |
| if(i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py); |
| }); |
| ctx.stroke(); |
| } |
| |
| |
| countEl.textContent = samples.length.toLocaleString(); |
| if(samples.length>0){ |
| const m = mean(samples); const v = variance(samples,m); const sd = Math.sqrt(v); |
| sMean.textContent = Number.isFinite(m)?m.toFixed(4):'β'; |
| sVar.textContent = Number.isFinite(v)?v.toFixed(4):'β'; |
| sSd.textContent = Number.isFinite(sd)?sd.toFixed(4):'β'; |
| const D = computeKS(); ksEl.textContent = isNaN(D)?'β':D.toFixed(4); |
| } else { |
| sMean.textContent=sVar.textContent=sSd.textContent=ksEl.textContent='β'; |
| } |
| } |
| |
| |
| $('sample').addEventListener('click', ()=>{ |
| const n = Number(nEl.value)||1000; |
| sampleN(n); redraw(); |
| }); |
| $('more').addEventListener('click', ()=>{ sampleN(10000); redraw(); }); |
| $('reset').addEventListener('click', ()=>{ samples=[]; redraw(); }); |
| |
| |
| redraw(); |
| </script> |
| </body> |
| </html> |
|
|