Average percentages with the right weight

HTML for this example

Select the code and use your copy shortcut. Changes in this preview are temporary.

<!doctype html><html lang="en-US"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Average percentages with the right weight</title><style>*{box-sizing:border-box} :root{color-scheme:light;--ink:#20303b;--muted:#536570;--line:#d3dce1;--accent:#245c76;--paper:#fff;--soft:#edf3f6}body{margin:0;background:#f3f5f5;color:var(--ink);font:16px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{max-width:1080px;margin:auto;padding:30px 32px 64px}header{margin-bottom:20px}h1{font-size:clamp(1.5rem,3vw,2.15rem);line-height:1.18;letter-spacing:-.035em;margin:0 0 8px}h2{font-size:1.22rem;line-height:1.3;margin:0 0 16px}h3{font-size:1rem;margin:0 0 8px}p{margin:8px 0 16px}small,.muted{color:var(--muted)}.eyebrow{font-size:.76rem;letter-spacing:.13em;text-transform:uppercase;font-weight:750;color:var(--accent);margin-bottom:8px}.panel{background:var(--paper);border:1px solid var(--line);padding:24px;border-radius:16px}.grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:24px}.stack{display:grid;gap:18px}.fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}label{display:block;font-size:.9rem;font-weight:650}input,select,textarea,button{font:inherit}input,select,textarea{display:block;width:100%;min-width:0;margin-top:6px;border:1px solid #8b9ca6;border-radius:7px;padding:10px 12px;background:white;color:var(--ink)}input,select{min-height:46px}textarea{resize:vertical}button{border:1px solid var(--accent);border-radius:7px;padding:10px 16px;min-height:44px;background:var(--accent);color:white;cursor:pointer;font-weight:650}button.secondary{background:white;color:var(--accent)}button:hover{filter:brightness(.94)}:focus-visible{outline:3px solid #d06e1c;outline-offset:3px}.actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:18px}.answer{background:var(--ink);color:white;padding:26px;border-radius:14px}.answer .muted{color:#d9e5eb}.big{font-size:clamp(2.1rem,5vw,3.8rem);font-weight:750;line-height:1.16;letter-spacing:-.045em;margin:8px 0;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.subresult{font-size:1.2rem;font-weight:650} .formula{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere}.note{border-left:3px solid #b47029;padding:10px 14px;background:#fff5e5;margin:14px 0;color:#66451b}.error{color:#a22621;font-weight:650;min-height:24px;margin:8px 0}.below{margin-top:28px}table{border-collapse:collapse;width:100%;font-size:.94rem}th{text-align:left;color:var(--muted);font-weight:650;font-size:.84rem}th,td{padding:11px 10px;border-bottom:1px solid var(--line);vertical-align:top}th:first-child,td:first-child{padding-left:0}th:last-child,td:last-child{padding-right:0}details{border-top:1px solid var(--line);padding:16px 0}summary{cursor:pointer;font-weight:650}svg{display:block;width:100%;height:auto}.tag{display:inline-block;font-size:.78rem;font-weight:650;padding:3px 8px;border-radius:30px;background:var(--soft);color:var(--accent)}.rule{height:1px;background:var(--line);margin:20px 0}.checklabel{display:flex;gap:10px;align-items:flex-start;font-weight:500}.checklabel input{width:20px;min-height:20px;flex:none;margin:2px 0}pre{white-space:pre-wrap;word-break:break-word;font: .86rem/1.6 ui-monospace,Consolas,monospace;background:#edf1f3;padding:20px;border-radius:8px}a{color:var(--accent)}[hidden]{display:none!important}@media(max-width:720px){main{padding:22px 20px 40px}.grid{grid-template-columns:1fr;gap:18px}.panel{padding:20px}.answer{padding:22px}.big{font-size:2.65rem}}@media(max-width:400px){main{padding:18px 16px 36px}.fields{gap:12px}th,td{padding:9px 6px}.panel{padding:16px}.big{font-size:2.25rem}}@media print{body{background:white}main{max-width:none;padding:0}.actions,button{display:none}.panel{break-inside:avoid}details>*{display:block}}:root{--accent:#5e4b82}.results-three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));background:#fff;border-top:5px solid #796697}.results-three>div{padding:22px;border-right:1px solid var(--line)}.results-three h2{font-size:.95rem;margin-bottom:10px}.results-three b{display:block;font-size:clamp(1.35rem,2.8vw,2.15rem);letter-spacing:-.04em;overflow-wrap:anywhere;color:#594779}.results-three span{display:block;color:var(--muted);font-size:.82rem;margin-top:8px}.group-ledger{margin:22px 0}.group-row{display:grid;grid-template-columns:55px minmax(0,1.5fr) minmax(0,.7fr) minmax(0,1.2fr);gap:14px;align-items:start;border-top:1px solid var(--line);padding:20px 0}.group-row h3{margin-top:32px}.row-state{grid-column:2/-1;color:var(--muted);font-size:.85rem}.group-row input{padding:8px}.group-row label{font-size:.8rem}@media(max-width:720px){.results-three{grid-template-columns:1fr}.results-three>div{padding:16px 20px;border-bottom:1px solid var(--line);display:grid;grid-template-columns:1.1fr 1fr;gap:6px 12px}.results-three span{grid-column:1/-1;margin:0}.results-three h2{margin:3px 0}.results-three b{font-size:1.6rem}.group-row{grid-template-columns:1fr 1fr}.group-row h3{margin:0;grid-column:1/-1}.group-row .count-fields,.group-row .rate-fields{grid-column:1/-1}.row-state{grid-column:1/-1}}</style></head><body><main><header><div class="eyebrow">Group rates · assignment counts · custom weights</div><h1>Average percentages with the right weight</h1></header>
<p>Compare what happens when each group, each assignment, or each declared weight counts equally.</p>
<section class="results-three" aria-live="polite"><div><h2>Equal-row mean</h2><b id="equal">70%</b><span id="equalScope">2 available group rates</span></div><div><h2>Pooled count rate</h2><b id="pooled">83.333333%</b><span id="poolScope">10 / 12 assignments</span></div><div><h2>Custom weighted mean</h2><b id="weighted">76.666667%</b><span id="weightScope">Declared weights 2 : 1</span></div></section><p id="coverage" class="note" role="status">Incomplete coverage: G03 is missing; full three-group aggregate unavailable.</p>
<section class="panel below"><div class="fields"><label for="sourceMode">Source data<select id="sourceMode"><option value="counts">Completed / assigned counts</option><option value="rates">Percentages only</option></select></label><label for="basis">Common basis<input id="basis" value="Disjoint assignment groups · October 13, 2026"></label></div><div id="ledger" class="group-ledger"><section class="group-row"><h3>G01</h3><div class="count-fields fields"><label for="c0">Completed<input id="c0" inputmode="decimal" value="9" ></label><label for="n0">Assigned<input id="n0" inputmode="decimal" value="10" ></label></div><div class="rate-fields" hidden><label for="r0">Rate (%)<input id="r0" inputmode="decimal" value="90" ></label></div><label for="w0">Custom weight<input id="w0" inputmode="decimal" value="2" ></label><label for="reason0">If weight is 0, record why<input id="reason0" value=""></label><div id="rowState0" class="row-state"></div></section><section class="group-row"><h3>G02</h3><div class="count-fields fields"><label for="c1">Completed<input id="c1" inputmode="decimal" value="1" ></label><label for="n1">Assigned<input id="n1" inputmode="decimal" value="2" ></label></div><div class="rate-fields" hidden><label for="r1">Rate (%)<input id="r1" inputmode="decimal" value="50" ></label></div><label for="w1">Custom weight<input id="w1" inputmode="decimal" value="1" ></label><label for="reason1">If weight is 0, record why<input id="reason1" value=""></label><div id="rowState1" class="row-state"></div></section><section class="group-row"><h3>G03</h3><div class="count-fields fields"><label for="c2">Completed<input id="c2" inputmode="decimal" value="" ></label><label for="n2">Assigned<input id="n2" inputmode="decimal" value="" ></label></div><div class="rate-fields" hidden><label for="r2">Rate (%)<input id="r2" inputmode="decimal" value="" ></label></div><label for="w2">Custom weight<input id="w2" inputmode="decimal" value="" ></label><label for="reason2">If weight is 0, record why<input id="reason2" value=""></label><div id="rowState2" class="row-state"></div></section></div><label class="checklabel" for="comparable"><input type="checkbox" id="comparable" checked>Groups are disjoint and use the same assignment-count basis.</label><div class="actions"><button type="button" id="calculate">Recalculate</button><button type="button" id="reset" class="secondary">Reset example</button></div><p id="error" role="status" class="error"></p></section>
<section class="below panel"><h2>Calculation record</h2><p id="derive" class="formula"></p><p>Custom weights are separately declared; their real purpose and authorization need confirmation. Weighting by assigned counts reproduces the pooled rate. Percentages alone cannot reconstruct pooled counts.</p></section>
<details class="below"><summary>Missing data, zero weights, and limits</summary><p>Results always name their available-row scope. Missing G03 stays in the ledger. A 0/0 row is undefined; 0 completed with positive assigned is a defined 0%. In custom mode only, an explicit zero weight excludes a row with a recorded reason. Missing weights never become zero. A zero total weight has no weighted mean.</p><p>Counts must be integers with 0 ≤ completed ≤ assigned ≤ 1,000,000. Rate values accept 0–100%, up to six decimal places; weights accept nonnegative values up to 1,000,000 with six places. A partially entered or invalid row clears results. Final displays are rounded to six places from unrounded values. These fictional records are not staff or student results, grades, or policy. Entries are not saved.</p></details></main><script>
const $=id=>document.getElementById(id);const put=(id,text)=>{$(id).textContent=text};const fmt=(x,n=6)=>{if(!Number.isFinite(x))throw Error('Result exceeds the numeric range.');let v=(Math.round(Math.abs(x)*10**n)/10**n)*(x<0?-1:1);return Object.is(v,-0)?'0':v.toFixed(n).replace(/\.?0+$/,'')||'0'};const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function decimal(s,{max=1000000,signed=false,places=6}={}){s=String(s).trim();if(!s)throw Error('Enter a value; a blank field is not zero.');const r=new RegExp('^'+(signed?'[+-]?':'')+'(?:[0-9]+(?:\\.[0-9]{1,'+places+'})?|\\.[0-9]{1,'+places+'})$');if(!r.test(s))throw Error('Use a '+(signed?'signed':'nonnegative')+' US decimal with up to '+places+' decimal places; no commas or exponents.');const v=Number(s);if(!Number.isFinite(v)||Math.abs(v)>max)throw Error('Enter a value within '+(signed?'-':'0 to ')+max+'.');return v;}
function scaled(s){decimal(s);const [a,b='']=String(s).trim().split('.');return BigInt(a||'0')*1000000n+BigInt(b.padEnd(6,'0'));}
function rational(n,d,places=6){if(d===0n)throw Error('Zero denominator');let neg=(n<0n)!=(d<0n);n=n<0n?-n:n;d=d<0n?-d:d;const scale=10n**BigInt(places),v=n*scale,q=v/d,r=v%d;let rounded=q+(r*2n>=d?1n:0n);let s=(rounded/scale).toString();const f=(rounded%scale).toString().padStart(places,'0').replace(/0+$/,'');if(f)s+='.'+f;return {text:(neg&&rounded!==0n?'-':'')+s,approx:r!==0n};}
function bind(ids,fn){ids.forEach(id=>{ $(id).addEventListener('input',fn);$(id).addEventListener('change',fn); });}

function aggregate(rows,mode,comparable){let rates=[],valid=[],missing=[],undefinedRows=[],issues=[],weighted=[],weightMissing=[];rows.forEach((r,i)=>{const id='G0'+(i+1);let rate=null;if(mode==='counts'){if(r.c.trim()===''&&r.n.trim()===''){missing.push(id);}else{if(!/^\d+$/.test(r.c)||!/^\d+$/.test(r.n))throw Error(id+': enter both counts as nonnegative integers.');const c=Number(r.c),n=Number(r.n);if(c>n||n>1000000)throw Error(id+': require completed ≤ assigned ≤ 1,000,000.');if(n===0)undefinedRows.push(id);else{rate=c/n*100;valid.push({c,n,id});}}}else{if(r.r.trim()==='')missing.push(id);else rate=decimal(r.r,{max:100});}if(rate!==null)rates.push({rate,id});let w=null;if(r.w.trim()!=='')w=decimal(r.w);if(w===0){if(!r.reason.trim())throw Error(id+': a zero weight requires a reason.');issues.push(id+' excluded by declared zero weight: '+r.reason);}else if(rate!==null&&w!==null)weighted.push({rate,w,id});else weightMissing.push(id);});
return {equal:rates.length?rates.reduce((s,r)=>s+r.rate,0)/rates.length:null,pooled:mode==='counts'&&comparable&&valid.length?valid.reduce((s,r)=>s+r.c,0)/valid.reduce((s,r)=>s+r.n,0)*100:null,weighted:weighted.length?weighted.reduce((s,r)=>s+r.rate*r.w,0)/weighted.reduce((s,r)=>s+r.w,0):null,rates,valid,missing,undefinedRows,weightMissing,issues,weightedRows:weighted};}
function update(){const mode=$('sourceMode').value;document.querySelectorAll('.count-fields').forEach(el=>el.hidden=mode!=='counts');document.querySelectorAll('.rate-fields').forEach(el=>el.hidden=mode==='counts');const rows=[0,1,2].map(i=>({c:$('c'+i).value,n:$('n'+i).value,r:$('r'+i).value,w:$('w'+i).value,reason:$('reason'+i).value}));try{const r=aggregate(rows,mode,$('comparable').checked);for(const key of ['equal','pooled','weighted'])put(key,r[key]===null?'Unavailable':fmt(r[key])+'%');put('equalScope',r.rates.length+' available group rate(s)');put('poolScope',mode==='rates'?'Counts not supplied':!$('comparable').checked?'Confirm disjoint, comparable groups':r.valid.reduce((s,r)=>s+r.c,0)+' / '+r.valid.reduce((s,r)=>s+r.n,0)+' available assignments');put('weightScope',r.weightedRows.length+' positive-weight available row(s)');const missing=[...r.missing,...r.undefinedRows];put('coverage',(missing.length?'Incomplete coverage: '+missing.join(', ')+' '+(r.undefinedRows.length?'missing or undefined':'missing')+'; full three-group aggregate unavailable.':'All three source rows covered.')+(r.weightMissing.length?' Custom weighted coverage incomplete: '+r.weightMissing.join(', ')+'.':'')+(r.issues.length?' '+r.issues.join('; '):''));rows.forEach((row,i)=>{const id='G0'+(i+1),rt=r.rates.find(x=>x.id===id);put('rowState'+i,rt?'Source rate: '+fmt(rt.rate)+'%':r.undefinedRows.includes(id)?'Undefined: 0 assigned, not a 0% rate':'Missing source data');});put('derive','Equal-row: ('+r.rates.map(r=>fmt(r.rate)).join(' + ')+') / '+r.rates.length+'. '+(mode==='counts'?'Pooled: sum completed / sum assigned × 100. ':'Pooled: unavailable without counts. ')+'Custom: sum(rate × declared weight) / sum(weight). Display rounded to six places where needed.');put('error','');}catch(e){['equal','pooled','weighted'].forEach(id=>put(id,'—'));['equalScope','poolScope','weightScope','derive'].forEach(id=>put(id,''));put('coverage','Current results unavailable; fix the source row or weight.');[0,1,2].forEach(i=>put('rowState'+i,''));put('error',e.message);}}
bind(['sourceMode','comparable',...[0,1,2].flatMap(i=>['c','n','r','w','reason'].map(p=>p+i))],update);$('calculate').addEventListener('click',update);$('reset').addEventListener('click',()=>{[['9','10','90','2'],['1','2','50','1'],['','','','']].forEach((r,i)=>{['c','n','r','w'].forEach((p,j)=>$(p+i).value=r[j]);$('reason'+i).value=''});$('sourceMode').value='counts';$('comparable').checked=true;update()});globalThis.Tool={aggregate,update};update();
</script></body></html>