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;
留言
張貼留言