Duke Industry Statistical Symposium 2026
Advanced Quantitative Sciences, Novartis Pharmaceuticals, East Hanover, NJ
April 10, 2026
When are accelerated plans considered?
How are CDPs accelerated?
By bypassing traditional steps:
Quantifying risk
Acceleration entails risk — the key challenge for statisticians is to quantify and communicate the level of risk
In an idealized situation where we evaluate the treatment effect for the same population, endpoints, control arms at each stage …
Given we are starting stage , what is ?
We can answer this question only with “if-then” analyses, where the “if” is an assumption about the true treatment effect.
Shortcoming: how plausible are these scenarios?
Allows us to incorporate beliefs and uncertainty about treatment effect through a prior, which (in the idealized situation) can be easily updated as data accumulates
If we begin (before phase-2) with a uniform prior for the treatment effect (from zero to 150% of the TPP):
Two key risk metrics for decision makers:
with , , futility boundary , and success region .
Ingredients:
A suitable prior for the treatment effect — difficult yet crucial when Phase 2 data is absent (next slide)
Interim and final readouts share patients correlated at square root of information fraction
Monte Carlo evaluation: draw , then jointly simulate from the BVN
From MC draws to PoS:
PoS today = fraction of all draws where interim passes and final succeeds, and regulatory approval is obtained
PoS post futility = based on restriction to draws with passed futility — equivalent to (Dragalin 2026)
Passage of futility is a pre-posterior Bayesian update with a censored observation.
Problem: Before we’ve seen Phase-2 data — what prior should we use for the treatment effect?
Idea Hampson et al. (2022): Build a two-component mixture prior :
Calibration: choose the mixture weight so that the prior-predictive probability of a program succeeding across a standardized series of remaining trials matches the industry-wide historical success rate for similar programs.
densityData = FileAttachment("resources/density_grid.json").json()
// Extract unique values for selectors
boundaries = [...new Set(densityData.posteriors.map(d => d.futility_boundary))].sort((a, b) => a - b)
infoFracs = [...new Set(densityData.posteriors.map(d => d.futility_information_fraction))].sort((a, b) => a - b)
// Display labels for boundary slider
boundaryLabels = new Map(boundaries.map(b =>
[b, b < 0 ? "None" : `${(b * 100).toFixed(0)}%`]
))How much does the futility analysis de-risk? Try adjusting the design:
viewof boundaryIdx = {
const div = html`<div style="margin-bottom:12px; text-align:center;">
<label style="font-size:13px;font-weight:600;">Futility boundary: <span id="boundary-val" style="color:#ff4e00;">${boundaryLabels.get(boundaries[boundaries.length - 1])}</span></label><br/>
<input type="range" min="0" max="${boundaries.length - 1}" step="1" value="${boundaries.length - 1}" style="width:300px; accent-color:#ff4e00;">
</div>`;
const input = div.querySelector("input");
const label = div.querySelector("#boundary-val");
div.value = +input.value;
input.oninput = () => {
div.value = +input.value;
label.textContent = boundaryLabels.get(boundaries[Math.round(+input.value)]);
div.dispatchEvent(new Event("input"));
};
return div;
}
viewof ifIdx = {
const div = html`<div style="margin-bottom:12px; text-align:center;">
<label style="font-size:13px;font-weight:600;">Info. fraction: <span id="if-val" style="color:#ff4e00;">${(infoFracs[0] * 100).toFixed(0)}%</span></label><br/>
<input type="range" min="0" max="${infoFracs.length - 1}" step="1" value="0" style="width:300px; accent-color:#ff4e00;">
</div>`;
const input = div.querySelector("input");
const label = div.querySelector("#if-val");
div.value = +input.value;
input.oninput = () => {
div.value = +input.value;
label.textContent = `${(infoFracs[Math.round(+input.value)] * 100).toFixed(0)}%`;
div.dispatchEvent(new Event("input"));
};
return div;
}Plot.plot({
width: 620,
height: 290,
marginLeft: 50,
marginBottom: 40,
marginTop: 25,
style: { fontSize: "12px", backgroundColor: "#fcfcfc" },
x: {
label: "Treatment effect (risk difference)",
domain: [-0.2, 0.5]
},
y: {
label: "Density",
domain: [0, 3.5],
grid: true
},
color: {
domain: ["Prior (today)", "After passing interim"],
range: ["#161616", "#ff4e00"],
legend: true
},
marks: [
Plot.text([{}], {
x: 0.15, y: 3.5, text: ["Bayesian update to the benchmark prior"],
dy: -10, fontSize: 14, fontWeight: "600", fill: "#161616"
}),
Plot.areaY(priorData, {
x: "risk_difference", y: "density",
fill: "#161616", fillOpacity: 0.12, curve: "basis"
}),
Plot.areaY(postData, {
x: "risk_difference", y: "density",
fill: "#ff4e00", fillOpacity: 0.18, curve: "basis"
}),
Plot.lineY(priorData, {
x: "risk_difference", y: "density",
stroke: "#161616", strokeWidth: 2, curve: "basis"
}),
Plot.lineY(postData, {
x: "risk_difference", y: "density",
stroke: "#ff4e00", strokeWidth: 2.5, curve: "basis"
}),
Plot.ruleX([0], { stroke: "#888888", strokeDasharray: "4,3", strokeWidth: 1 }),
Plot.ruleX([0.25], { stroke: "#ff4e00", strokeDasharray: "6,3", strokeWidth: 1.5 }),
Plot.text([{}], {
x: 0.005, y: 0, text: ["H₀"],
dy: -5, fontSize: 11, fill: "#888888"
}),
Plot.text([{}], {
x: 0.255, y: 0, text: ["TPP"],
dy: -5, fontSize: 11, fill: "#ff4e00"
})
]
})summaryText = {
if (!summ || !summ.p_pass_interim) return md`*Adjust sliders*`;
const pct = v => v != null ? `${(v * 100).toFixed(0)}%` : "—";
return md`
<span style="color: #161616; font-weight: 600;">Prior (today):</span>
P(pass interim): **${pct(summ.p_pass_interim)}**
| P(significant) | P(meet TPP) | PoS |
|:--------------:|:-----------:|:---:|
| ${pct(summ.p_sig_final)} | ${pct(summ.p_tpp_final)} | ${pct(summ.p_success)} |
<span style="color: #ff4e00; font-weight: 600;">After passing interim:</span>
| P(significant) | P(meet TPP) | PoS |
|:--------------:|:-----------:|:---:|
| ${pct(summ.p_sig_given_pass)} | ${pct(summ.p_tpp_given_pass)} | ${pct(summ.p_success_given_pass)} |
`;
}selectedBoundary = boundaries[Math.round(boundaryIdx)]
selectedIF = infoFracs[Math.round(ifIdx)]
selectedPosterior = densityData.posteriors.find(
d => d.futility_boundary === selectedBoundary &&
d.futility_information_fraction === selectedIF
)
priorData = densityData.prior.map(d => ({
risk_difference: d.risk_difference,
density: d.density,
group: "Prior (today)"
}))
postData = selectedPosterior
? selectedPosterior.density.map(d => ({
risk_difference: d.risk_difference,
density: d.density,
group: "After passing interim (conditional)"
}))
: []
summ = selectedPosterior ? selectedPosterior.summary : {}%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#f5f5f5', 'primaryTextColor': '#161616', 'primaryBorderColor': '#dadada', 'lineColor': '#161616', 'secondaryColor': '#dadada', 'tertiaryColor': '#fcfcfc', 'fontFamily': 'system-ui, sans-serif'}}}%%
flowchart LR
A["Start Ph3<br/>Cost C₁"] --> B{"Pass futility?"}
B -- "Yes · P(pass)" --> C["Continue<br/>Cost C₂"]
B -- "No · 1−P(pass)" --> D["Stop early<br/>Save C₂ + C₃"]
C --> E{"Final success?"}
E -- "Yes · P(pivotal success|pass)" --> F["Submit<br/>Cost C₃"]
E -- "No" --> G["Write-off<br/>C₁ + C₂"]
F --> H{"Approved?"}
H -- "Yes · P(approval|pivotal success)" --> I["Revenue V"]
H -- "No" --> J["No approval<br/>Write-off C₁ + C₃"]
Conditional assurance is the natural prospective metric for futility-gated programs — it handles uncertainty about without unblinding
The benchmark-calibrated prior is a principled fallback when Phase 2 data is absent; data sharpens it when available
For straight-to-phase-3 accelerations with stringent de-risking via futility rules: joint simulation approach (BVN with ) provides a complete picture: P(pass), conditional PoS, and overall PoS in a single framework
No single metric suffices for communicating about risk — but PoS is central because it feeds both expectations and portfolio valuation
Transparent quantification of trade-offs (PoS loss vs. risk discharged) enables informed design decisions across diverse stakeholders
Quantifying Risk for Accelerated Drug Development