貝氏分析計算器

<!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>

留言

這個網誌中的熱門文章

可轉移性、普遍性、代表性和外部有效性

頻率學派 vs 貝氏學派

貝氏分析計算器