Webapp: 臨床試驗的貝氏分析

import React, { useState, useCallback, useMemo } from 'react';

import { Tab } from '@headlessui/react';

import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer } from 'recharts';

import { HelpCircle } from 'lucide-react';


const BayesianAnalysis = () => {

    // Core state

    const [studyType, setStudyType] = useState('hazardRatio');

    const [pointEstimate, setPointEstimate] = useState(0.76);

    const [confidenceInterval, setConfidenceInterval] = useState({ lower: 0.55, upper: 1.02 });

    const [events, setEvents] = useState({

        treatment: { events: 34, total: 100 },

        control: { events: 43, total: 100 }

    });

    

    // Added user-defined MCID and value of interest

    const [mcid, setMcid] = useState(0.8);

    const [valueOfInterest, setValueOfInterest] = useState(0.7);

    

    // Prior distribution parameters

    const [priorParams, setPriorParams] = useState({

        mean: 1.0,

        sd: 0.42,

        targetValue: 0.9,

        credibleInterval: 0.95

    });


    // Statistical utility functions

    const normalPDF = useCallback((x, mean, sd) => {

        return Math.exp(-0.5 * Math.pow((x - mean) / sd, 2)) / (sd * Math.sqrt(2 * Math.PI));

    }, []);


    const normalCDF = useCallback((x, mean, sd) => {

        return 0.5 * (1 + erf((x - mean) / (sd * Math.sqrt(2))));

    }, []);


    const erf = useCallback((x) => {

        const a1 =  0.254829592;

        const a2 = -0.284496736;

        const a3 =  1.421413741;

        const a4 = -1.453152027;

        const a5 =  1.061405429;

        const p   0.3275911;


        const sign = x < 0 ? -1 : 1;

        x = Math.abs(x);


        const t = 1.0/(1.0 + p*x);

        const y = 1.0 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t*Math.exp(-x*x);


        return sign * y;

    }, []);


    // Calculate likelihood parameters

    const likelihoodParams = useMemo(() => {

        const logEstimate = Math.log(pointEstimate);

        const logSE = (Math.log(confidenceInterval.upper) - logEstimate) / 1.96;

        

        return {

            mean: logEstimate,

            sd: logSE

        };

    }, [pointEstimate, confidenceInterval]);


    // Calculate posterior parameters

    const posteriorParams = useMemo(() => {

        const priorPrecision = 1 / Math.pow(priorParams.sd, 2);

        const likelihoodPrecision = 1 / Math.pow(likelihoodParams.sd, 2);

        const posteriorPrecision = priorPrecision + likelihoodPrecision;

        

        const posteriorMean = (Math.log(priorParams.mean) * priorPrecision + 

                              likelihoodParams.mean * likelihoodPrecision) / posteriorPrecision;

        const posteriorSD = Math.sqrt(1 / posteriorPrecision);


        return {

            mean: posteriorMean,

            sd: posteriorSD

        };

    }, [priorParams, likelihoodParams]);


    // Generate distribution data

    const distributionData = useMemo(() => {

        const data = [];

        for (let x = 0.1; x <= 3; x += 0.02) {

            const logx = Math.log(x);

            data.push({

                x: x,

                prior: normalPDF(logx, Math.log(priorParams.mean), priorParams.sd),

                likelihood: normalPDF(logx, likelihoodParams.mean, likelihoodParams.sd),

                posterior: normalPDF(logx, posteriorParams.mean, posteriorParams.sd)

            });

        }

        return data;

    }, [normalPDF, priorParams, likelihoodParams, posteriorParams]);


    // Calculate posterior probabilities

    const posteriorProbabilities = useMemo(() => {

        const probLessThan1 = normalCDF(0, posteriorParams.mean, posteriorParams.sd);

        const probLessThanTarget = normalCDF(

            Math.log(priorParams.targetValue), 

            posteriorParams.mean, 

            posteriorParams.sd

        );

        const probLessThanMCID = normalCDF(

            Math.log(mcid),

            posteriorParams.mean,

            posteriorParams.sd

        );

        const probLessThanValue = normalCDF(

            Math.log(valueOfInterest),

            posteriorParams.mean,

            posteriorParams.sd

        );

        

        const credibleInterval = {

            lower: Math.exp(posteriorParams.mean - 1.96 * posteriorParams.sd),

            upper: Math.exp(posteriorParams.mean + 1.96 * posteriorParams.sd)

        };


        return {

            probLessThan1,

            probLessThanTarget,

            probLessThanMCID,

            probLessThanValue,

            credibleInterval

        };

    }, [normalCDF, posteriorParams, priorParams.targetValue, mcid, valueOfInterest]);


    // Calculate frequentist metrics

    const frequentistMetrics = useMemo(() => {

        const z = Math.abs(Math.log(pointEstimate)) / 

                 ((Math.log(confidenceInterval.upper) - Math.log(pointEstimate)) / 1.96);

        const pValue = 2 * (1 - normalCDF(z, 0, 1));

        

        return {

            zScore: z,

            pValue: pValue

        };

    }, [pointEstimate, confidenceInterval, normalCDF]);


    return (

        <div className="max-w-7xl mx-auto p-4">

            <h1 className="text-3xl font-bold mb-8">Bayesian Clinical Trial Analysis</h1>

            

            <Tab.Group>

                <Tab.List className="flex space-x-1 rounded-xl bg-blue-900/20 p-1">

                    <Tab className={({ selected }) =>

                        `w-full rounded-lg py-2.5 text-sm font-medium leading-5

                         ${selected 

                            ? 'bg-white shadow text-blue-700'

                            : 'text-blue-500 hover:bg-white/[0.12] hover:text-blue-600'

                        }`

                    }>

                        Study Data

                    </Tab>

                    <Tab className={({ selected }) =>

                        `w-full rounded-lg py-2.5 text-sm font-medium leading-5

                         ${selected 

                            ? 'bg-white shadow text-blue-700'

                            : 'text-blue-500 hover:bg-white/[0.12] hover:text-blue-600'

                        }`

                    }>

                        Prior Distribution

                    </Tab>

                    <Tab className={({ selected }) =>

                        `w-full rounded-lg py-2.5 text-sm font-medium leading-5

                         ${selected 

                            ? 'bg-white shadow text-blue-700'

                            : 'text-blue-500 hover:bg-white/[0.12] hover:text-blue-600'

                        }`

                    }>

                        Results

                    </Tab>

                </Tab.List>


                <Tab.Panels className="mt-4">

                    <Tab.Panel className="rounded-xl bg-white p-4">

                        <div className="space-y-4">

                            <div>

                                <label className="block text-sm font-medium text-gray-700">

                                    Effect Type

                                    <HelpCircle className="inline-block ml-1 h-4 w-4 text-gray-400" />

                                </label>

                                <select 

                                    className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"

                                    value={studyType}

                                    onChange={(e) => setStudyType(e.target.value)}

                                >

                                    <option value="hazardRatio">Hazard Ratio</option>

                                    <option value="oddsRatio">Odds Ratio</option>

                                    <option value="riskRatio">Risk Ratio</option>

                                </select>

                            </div>


                            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">

                                <div>

                                    <label className="block text-sm font-medium text-gray-700">

                                        Point Estimate

                                    </label>

                                    <input

                                        type="number"

                                        className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"

                                        value={pointEstimate}

                                        onChange={(e) => setPointEstimate(parseFloat(e.target.value))}

                                        step="0.01"

                                    />

                                </div>


                                <div>

                                    <label className="block text-sm font-medium text-gray-700">

                                        95% Confidence Interval

                                    </label>

                                    <div className="grid grid-cols-2 gap-2">

                                        <input

                                            type="number"

                                            className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"

                                            value={confidenceInterval.lower}

                                            onChange={(e) => setConfidenceInterval(prev => ({

                                                ...prev,

                                                lower: parseFloat(e.target.value)

                                            }))}

                                            step="0.01"

                                        />

                                        <input

                                            type="number"

                                            className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"

                                            value={confidenceInterval.upper}

                                            onChange={(e) => setConfidenceInterval(prev => ({

                                                ...prev,

                                                upper: parseFloat(e.target.value)

                                            }))}

                                            step="0.01"

                                        />

                                    </div>

                                </div>

                            </div>


                            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">

                                <div>

                                    <label className="block text-sm font-medium text-gray-700">

                                        Minimal Clinically Important Difference (MCID)

                                        <HelpCircle className="inline-block ml-1 h-4 w-4 text-gray-400" />

                                    </label>

                                    <input

                                        type="number"

                                        className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"

                                        value={mcid}

                                        onChange={(e) => setMcid(parseFloat(e.target.value))}

                                        step="0.01"

                                    />

                                </div>

                                <div>

                                    <label className="block text-sm font-medium text-gray-700">

                                        Value of Interest

                                        <HelpCircle className="inline-block ml-1 h-4 w-4 text-gray-400" />

                                    </label>

                                    <input

                                        type="number"

                                        className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"

                                        value={valueOfInterest}

                                        onChange={(e) => setValueOfInterest(parseFloat(e.target.value))}

                                        step="0.01"

                                    />

                                </div>

                            </div>

                        </div>

                    </Tab.Panel>


                    <Tab.Panel className="rounded-xl bg-white p-4">

                        <div className="space-y-4">

                            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">

                                <div>

                                    <label className="block text-sm font-medium text-gray-700">

                                        Prior Mean

                                    </label>

                                    <input

                                        type="number"

                                        className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"

                                        value={priorParams.mean}

                                        onChange={(e) => setPriorParams(prev => ({

                                            ...prev,

                                            mean: parseFloat(e.target.value)

                                        }))}

                                        step="0.1"

                                    />

                                </div>


                                <div>

                                    <label className="block text-sm font-medium text-gray-700">

                                        Prior Standard Deviation

                                    </label>

                                    <input

                                        type="number"

                                        className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"

                                        value={priorParams.sd}

                                        onChange={(e) => setPriorParams(prev => ({

                                            ...prev,

                                            sd: parseFloat(e.target.value)

                                        }))}

                                        step="0.05"

                                    />

                                </div>

                            </div>

                        </div>

                    </Tab.Panel>


                    <Tab.Panel className="rounded-xl bg-white p-4">

                        <div className="space-y-8">

                            <div className="h-96">

                                <ResponsiveContainer width="100%" height="100%">

                                    <LineChart data={distributionData}>

                                        <XAxis 

                                            dataKey="x" 

                                            label={{ value: 'Effect Size', position: 'bottom' }}

                                        />

                                        <YAxis 

                                            label={{ 

                                                value: 'Density', 

                                                angle: -90, 

                                                position: 'insideLeft' 

                                            }}

                                        />

                                        <Tooltip />

                                        <Legend />

                                        <Line 

                                            type="monotone" 

                                            dataKey="prior" 

                                            stroke="#8884d8" 

                                            name="Prior"

                                        />

                                        <Line 

                                            type="monotone" 

                                            dataKey="likelihood" 

                                            stroke="#82ca9d" 

                                            name="Likelihood"

                                        />

                                        <Line 

                                            type="monotone" 

                                            dataKey="posterior" 

                                            stroke="#ffc658" 

                                            name="Posterior"

                                        />

                                    </LineChart>

                                </ResponsiveContainer>

                            </div>


                            <div className="bg-gray-50 p-4 rounded-lg">

                                <h3 className="text-lg font-medium mb-4">Posterior Probabilities</h3>

                                <div className="space-y-2">

                                    <p>

                                        Probability effect is beneficial (less than 1): 

                                        {(posteriorProbabilities.probLessThan1 * 100).toFixed(1)}%

                                    </p>

                                    <p>

                                        Probability effect is less than {priorParams.targetValue}: 

                                        {(posteriorProbabilities.probLessThanTarget * 100).toFixed(1)}%

                                    </p>

                                    <p>

                                        Probability effect is less than MCID ({mcid}): 

                                        {(posteriorProbabilities.probLessThanMCID * 100).toFixed(1)}%

                                    </p>

                                    <p>

                                        Probability effect is less than value of interest ({valueOfInterest}): 

                                        {(posteriorProbabilities.probLessThanValue * 100).toFixed(1)}%

                                    </p>

                                    <p>

                                        95% Credible Interval: 

                                        [{posteriorProbabilities.credibleInterval.lower.toFixed(2)}, 

                                        {posteriorProbabilities.credibleInterval.upper.toFixed(2)}]

                                    </p>

                                </div>

                            </div>


                            <div className="bg-gray-50 p-4 rounded-lg">

                                <h3 className="text-lg font-medium mb-4">Frequentist Analysis</h3>

                                <div className="space-y-2">

                                    <p>

                                        Z-score: {frequentistMetrics.zScore.toFixed(2)}

                                    </p>

                                    <p>

                                        P-value: {frequentistMetrics.pValue.toFixed(4)}

                                    </p>

                                </div>

                            </div>

                        </div>

                    </Tab.Panel>

                </Tab.Panels>

            </Tab.Group>

        </div>

    );

};


export default BayesianAnalysis;

留言

這個網誌中的熱門文章

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

頻率學派 vs 貝氏學派

貝氏分析計算器