貝氏分析計算器
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>貝氏分析計算器 / Bayesian Calculator</title>
<style>
body {
font-family: "Microsoft JhengHei", Arial, sans-serif;
max-width: 800px;
margin: 20px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.input-group {
margin-bottom: 20px;
}
.input-group label {
display: inline-block;
width: 200px;
}
input[type="number"] {
width: 100px;
padding: 5px;
margin: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
select, button {
padding: 8px 16px;
margin: 5px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: white;
cursor: pointer;
}
select {
background-color: white;
color: #333;
border: 1px solid #ddd;
}
button:hover {
background-color: #0056b3;
}
#results {
white-space: pre-wrap;
font-family: monospace;
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #ddd;
}
.error {
color: #721c24;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
margin: 10px 0;
padding: 10px;
border-radius: 4px;
}
.warning {
color: #856404;
background-color: #fff3cd;
border: 1px solid #ffeeba;
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
.mcid-input {
margin-top: 10px;
padding: 15px;
background-color: #e9ecef;
border-radius: 4px;
border: 1px solid #dee2e6;
}
.note {
font-size: 0.9em;
color: #666;
margin-top: 5px;
padding-left: 10px;
border-left: 3px solid #007bff;
}
.diagnostic-plot {
margin-top: 20px;
border: 1px solid #ddd;
padding: 10px;
border-radius: 4px;
}
#plotCanvas {
margin-top: 10px;
border: 1px solid #ddd;
}
.references-section {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ddd;
text-align: center;
}
.references-dropdown {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>貝氏分析計算器 / Bayesian Analysis Calculator</h1>
<div class="input-group">
<label>效應量類型 / Effect Size Type:</label>
<select id="effectType" onchange="updateInputFields()">
<option value="smd">標準化平均差 / Standardized Mean Difference</option>
<option value="or">勝算比 / Odds Ratio</option>
<option value="rr">風險比 / Risk Ratio</option>
<option value="hr">危險比 / Hazard Ratio</option>
</select>
</div>
<div id="smdInputs">
<div class="input-group">
<label>第一組 / Group 1:</label>
<input type="number" id="mean1" placeholder="平均值/Mean" step="any" required>
<input type="number" id="sd1" placeholder="標準差/SD" step="any" required>
<input type="number" id="n1" placeholder="樣本數/N" step="1" required min="2">
</div>
<div class="input-group">
<label>第二組 / Group 2:</label>
<input type="number" id="mean2" placeholder="平均值/Mean" step="any" required>
<input type="number" id="sd2" placeholder="標準差/SD" step="any" required>
<input type="number" id="n2" placeholder="樣本數/N" step="1" required min="2">
</div>
</div>
<div id="contingencyInputs" style="display: none;">
<div class="input-group">
<label>2x2列聯表 / 2x2 Table:</label>
<div>
<input type="number" id="cell_a" placeholder="a (事件/暴露)" step="1" min="0" required>
<input type="number" id="cell_b" placeholder="b (無事件/暴露)" step="1" min="0" required>
</div>
<div>
<input type="number" id="cell_c" placeholder="c (事件/對照)" step="1" min="0" required>
<input type="number" id="cell_d" placeholder="d (無事件/對照)" step="1" min="0" required>
</div>
</div>
</div>
<div id="hrInputs" style="display: none;">
<div class="input-group">
<label>危險比數據 / HR Data:</label>
<input type="number" id="hr" placeholder="HR" step="any" required min="0">
<input type="number" id="cri_lower" placeholder="CrI Lower" step="any" required min="0">
<input type="number" id="cri_upper" placeholder="CrI Upper" step="any" required min="0">
</div>
</div>
<div class="mcid-input">
<label>最小臨床重要差異 / MCID:</label>
<input type="number" id="mcid" placeholder="MCID" step="any" required>
<div class="note">
標準化平均差:負值表示有益 / For SMD: negative values indicate benefit<br>
比值:小於1表示有益 / For ratios: values less than 1 indicate benefit
</div>
</div>
<button onclick="calculateResults()">計算 / Calculate</button>
<div id="warnings"></div>
<div id="results"></div>
<div class="diagnostic-plot">
<canvas id="plotCanvas" width="600" height="400"></canvas>
</div>
<div class="references-section">
<div class="references-dropdown">
<label>延伸資料 / References:</label>
<select onchange="if(this.value) window.open(this.value, '_blank')">
<option value="">選擇參考資料 / Select Reference</option>
<option value="https://guhstats.blogspot.com/2025/01/blog-post.html">貝氏分析 (2025/01/30)</option>
</select>
</div>
</div>
</div>
<script>
function randn() {
let u = 0, v = 0;
while(u === 0) u = Math.random();
while(v === 0) v = Math.random();
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
function normalCDF(x, mean = 0, sd = 1) {
const z = (x - mean) / sd;
const t = 1 / (1 + 0.2316419 * Math.abs(z));
const d = 0.3989423 * Math.exp(-z * z / 2);
const p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
return z > 0 ? 1 - p : p;
}
function calculatePosteriorPredictive(posterior_mean, posterior_sd, effectType, n1, n2) {
const samples = 10000;
let predictions = [];
let test_statistics = [];
for (let i = 0; i < samples; i++) {
const effect_sample = posterior_mean + posterior_sd * randn();
if (effectType === 'smd') {
const new_diff = effect_sample + randn() * Math.sqrt(1/n1 + 1/n2);
predictions.push(new_diff);
test_statistics.push(Math.abs(new_diff));
} else {
const new_log_ratio = effect_sample + randn() * posterior_sd;
predictions.push(Math.exp(new_log_ratio));
test_statistics.push(Math.abs(new_log_ratio));
}
}
predictions.sort((a, b) => a - b);
const pi_lower = predictions[Math.floor(0.025 * samples)];
const pi_upper = predictions[Math.floor(0.975 * samples)];
const observed = effectType === 'smd' ?
Math.abs(posterior_mean) :
Math.abs(Math.log(Math.exp(posterior_mean)));
const ppp_value = test_statistics.filter(t => t >= observed).length / samples;
return {
pi_lower: pi_lower,
pi_upper: pi_upper,
ppp_value: ppp_value,
predictions: predictions
};
}
function updateInputFields() {
const effectType = document.getElementById('effectType').value;
document.getElementById('smdInputs').style.display = effectType === 'smd' ? 'block' : 'none';
document.getElementById('contingencyInputs').style.display =
(effectType === 'or' || effectType === 'rr') ? 'block' : 'none';
document.getElementById('hrInputs').style.display = effectType === 'hr' ? 'block' : 'none';
const mcidInput = document.getElementById('mcid');
mcidInput.value = effectType === 'smd' ? -0.2 : 0.8;
}
function validateInputs(effectType) {
const warnings = [];
if (effectType === 'smd') {
const n1 = parseInt(document.getElementById('n1').value);
const n2 = parseInt(document.getElementById('n2').value);
if (n1 < 2 || n2 < 2) {
throw new Error("每組至少需要2個樣本 / Each group needs at least 2 samples");
}
if (n1 < 30 || n2 < 30) {
warnings.push("小樣本量可能影響估計準確度 / Small sample size may affect estimation accuracy");
}
} else if (effectType === 'or' || effectType === 'rr') {
const cells = ['a', 'b', 'c', 'd'].map(id =>
parseInt(document.getElementById(`cell_${id}`).value));
if (cells.some(cell => cell === 0)) {
warnings.push("存在零細格,已應用連續性校正 / Zero cells present, continuity correction applied");
}
const total = cells.reduce((a, b) => a + b, 0);
if (total < 40) {
warnings.push("總樣本量較小,可能影響估計準確度 / Total sample size is small, may affect estimation accuracy");
}
} else if (effectType === 'hr') {
const hr = parseFloat(document.getElementById('hr').value);
const cri_lower = parseFloat(document.getElementById('cri_lower').value);
const cri_upper = parseFloat(document.getElementById('cri_upper').value);
if (hr <= 0 || cri_lower <= 0 || cri_upper <= 0) {
throw new Error("HR和可信區間必須為正值 / HR and CrI bounds must be positive");
}
if (cri_lower >= cri_upper) {
throw new Error("上限必須大於下限 / Upper bound must be greater than lower bound");
}
}
return warnings;
}
function calculateSMD() {
const mean1 = parseFloat(document.getElementById('mean1').value);
const mean2 = parseFloat(document.getElementById('mean2').value);
const sd1 = parseFloat(document.getElementById('sd1').value);
const sd2 = parseFloat(document.getElementById('sd2').value);
const n1 = parseInt(document.getElementById('n1').value);
const n2 = parseInt(document.getElementById('n2').value);
const pooled_sd = Math.sqrt(((n1 - 1) * sd1 * sd1 + (n2 - 1) * sd2 * sd2) / (n1 + n2 - 2));
let effect = (mean1 - mean2) / pooled_sd;
const df = n1 + n2 - 2;
const J = 1 - (3 / (4 * df - 1));
effect = effect * J;
const se = Math.sqrt((n1 + n2) / (n1 * n2) + effect * effect / (2 * (n1 + n2)));
return {
effect: effect,
se: se,
diagnostics: {
'pooled_sd': pooled_sd,
'hedges_correction': J,
'df': df,
'n1': n1,
'n2': n2
}
};
}
function calculateContingencyTable(type) {
let a = parseInt(document.getElementById('cell_a').value);
let b = parseInt(document.getElementById('cell_b').value);
let c = parseInt(document.getElementById('cell_c').value);
let d = parseInt(document.getElementById('cell_d').value);
const hasZero = (a === 0 || b === 0 || c === 0 || d === 0);
if (hasZero) {
a += 0.5;
b += 0.5;
c += 0.5;
d += 0.5;
}
let effect, se;
if (type === 'or') {
effect = Math.log((a * d) / (b * c));
se = Math.sqrt(1/a + 1/b + 1/c + 1/d);
} else {
const r1 = a / (a + b);
const r2 = c / (c + d);
effect = Math.log(r1 / r2);
se = Math.sqrt(1/a - 1/(a+b) + 1/c - 1/(c+d));
}
return {
effect: effect,
se: se,
diagnostics: {
'continuity_correction': hasZero,
'total_sample_size': a + b + c + d,
'n1': a + b,
'n2': c + d
}
};
}
function calculateHR() {
const hr = parseFloat(document.getElementById('hr').value);
const cri_lower = parseFloat(document.getElementById('cri_lower').value);
const cri_upper = parseFloat(document.getElementById('cri_upper').value);
const effect = Math.log(hr);
const se = (Math.log(cri_upper) - Math.log(cri_lower)) / (2 * 1.96);
return {
effect: effect,
se: se,
diagnostics: {
'original_hr': hr,
'cri_width': cri_upper - cri_lower,
'n1': 100,
'n2': 100
}
};
}
function calculatePosterior(effect, se, prior_sd, effectType, diagnostics) {
const posterior_sd = 1 / Math.sqrt(1/Math.pow(se, 2) + 1/Math.pow(prior_sd, 2));
const posterior_mean = (effect/Math.pow(se, 2)) * Math.pow(posterior_sd, 2);
const cri_lower = posterior_mean - 1.96 * posterior_sd;
const cri_upper = posterior_mean + 1.96 * posterior_sd;
const mcid = parseFloat(document.getElementById('mcid').value);
const mcid_value = effectType === 'smd' ? mcid : Math.log(mcid);
const prob_less_than_mcid = normalCDF(mcid_value, posterior_mean, posterior_sd);
const predictive = calculatePosteriorPredictive(
posterior_mean,
posterior_sd,
effectType,
diagnostics.n1,
diagnostics.n2
);
return {
mean: posterior_mean,
sd: posterior_sd,
cri_lower: cri_lower,
cri_upper: cri_upper,
prob_less_than_mcid: prob_less_than_mcid,
predictive: predictive
};
}
function drawPredictivePlot(predictions, observed, effectType) {
const canvas = document.getElementById('plotCanvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const margin = 50;
const width = canvas.width - 2 * margin;
const height = canvas.height - 2 * margin;
const bins = 30;
const hist = new Array(bins).fill(0);
const min = Math.min(...predictions);
const max = Math.max(...predictions);
const binWidth = (max - min) / bins;
predictions.forEach(p => {
const bin = Math.min(
Math.floor((p - min) / binWidth),
bins - 1
);
hist[bin]++;
});
const maxCount = Math.max(...hist);
ctx.beginPath();
ctx.moveTo(margin, margin);
ctx.lineTo(margin, canvas.height - margin);
ctx.lineTo(canvas.width - margin, canvas.height - margin);
ctx.stroke();
ctx.fillStyle = 'rgba(0, 123, 255, 0.5)';
hist.forEach((count, i) => {
const x = margin + (i * width / bins);
const barHeight = (count / maxCount) * height;
ctx.fillRect(
x,
canvas.height - margin - barHeight,
width / bins - 1,
barHeight
);
});
ctx.strokeStyle = 'red';
ctx.beginPath();
const x = margin + ((observed - min) / (max - min)) * width;
ctx.moveTo(x, margin);
ctx.lineTo(x, canvas.height - margin);
ctx.stroke();
ctx.fillStyle = 'black';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(effectType === 'smd' ? 'Effect Size' : 'Log Ratio',
canvas.width / 2, canvas.height - 10);
ctx.save();
ctx.translate(15, canvas.height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Density', 0, 0);
ctx.restore();
}
function formatResults(result, effectType) {
const priors = [
{sd: 0.2, name: "Strong Prior", nameCh: "強信念先驗"},
{sd: 0.42, name: "Default Prior", nameCh: "預設先驗"},
{sd: 2.0, name: "Weak Prior", nameCh: "弱信念先驗"}
];
let output = `${effectType.toUpperCase()} 分析結果 / Analysis Results:\n\n`;
output += `原始效應量估計 / Raw Effect Estimate:\n`;
output += `點估計 / Point Estimate: ${result.effect.toFixed(3)}`;
if (effectType !== 'smd') {
output += ` (exp: ${Math.exp(result.effect).toFixed(3)})`;
}
output += `\n標準誤 / Standard Error: ${result.se.toFixed(3)}\n\n`;
priors.forEach(prior => {
const posteriorResult = calculatePosterior(
result.effect,
result.se,
prior.sd,
effectType,
result.diagnostics
);
output += `${prior.nameCh} / ${prior.name}:\n`;
output += `後驗平均值 / Posterior Mean: ${posteriorResult.mean.toFixed(3)}`;
if (effectType !== 'smd') {
output += ` (exp: ${Math.exp(posteriorResult.mean).toFixed(3)})`;
}
output += '\n';
output += `95% 可信區間 / Credible Interval: [${posteriorResult.cri_lower.toFixed(3)}, ${posteriorResult.cri_upper.toFixed(3)}]`;
if (effectType !== 'smd') {
output += ` (exp: [${Math.exp(posteriorResult.cri_lower).toFixed(3)}, ${Math.exp(posteriorResult.cri_upper).toFixed(3)}])`;
}
output += '\n';
output += `95% 預測區間 / Predictive Interval: [${posteriorResult.predictive.pi_lower.toFixed(3)}, ${posteriorResult.predictive.pi_upper.toFixed(3)}]\n`;
output += `後驗預測p值 / Posterior Predictive p-value: ${posteriorResult.predictive.ppp_value.toFixed(3)}\n`;
output += `效應小於最小臨床重要差異的機率 / Pr(effect < MCID): ${(posteriorResult.prob_less_than_mcid * 100).toFixed(1)}%\n\n`;
if (prior.sd === 0.42) {
drawPredictivePlot(
posteriorResult.predictive.predictions,
result.effect,
effectType
);
}
});
if (result.diagnostics) {
output += "診斷資訊 / Diagnostics:\n";
for (let [key, value] of Object.entries(result.diagnostics)) {
if (typeof value === 'boolean') {
output += `${key}: ${value ? "是/Yes" : "否/No"}\n`;
} else if (typeof value === 'number') {
output += `${key}: ${value.toFixed(3)}\n`;
}
}
}
return output;
}
function calculateResults() {
const effectType = document.getElementById('effectType').value;
const warningsDiv = document.getElementById('warnings');
const resultsDiv = document.getElementById('results');
try {
const warnings = validateInputs(effectType);
let result;
switch(effectType) {
case 'smd':
result = calculateSMD();
break;
case 'or':
case 'rr':
result = calculateContingencyTable(effectType);
break;
case 'hr':
result = calculateHR();
break;
}
if (warnings.length > 0) {
warningsDiv.innerHTML = warnings.map(w =>
`<div class="warning">⚠️ ${w}</div>`
).join('');
} else {
warningsDiv.innerHTML = '';
}
resultsDiv.innerText = formatResults(result, effectType);
} catch(e) {
warningsDiv.innerHTML = `<div class="error">❌ ${e.message}</div>`;
resultsDiv.innerText = '';
}
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('mcid').value = -0.2;
const canvas = document.getElementById('plotCanvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'black';
ctx.font = '14px Arial';
ctx.textAlign = 'center';
ctx.fillText('Posterior Predictive Plot will appear here after calculation',
canvas.width/2, canvas.height/2);
});
</script>
</body>
</html>
留言
張貼留言