The Fagan nomogram (Fagan 1975) is a nomogram (Wikipedia contributors 2022) that computes the probability of the presence of some condition based on an imperfect test and varying pre-test probabilities. It is a very handy tool to understand Bayes’s Theorem “physically”. People typically have a sense that the less powerful the test, the less likely it is that a positive test result means the presence of the condition. But people are much less likely to grasp the role of the pre-test probability (Casscells, Schoenberger, and Graboys 1978).
As an illustration, consider the example illustrated in the default setting of the nomogram below. If only 10% of the population exhibit a particular kind of condition, then even if a test gives a ratio of true positives to false positives at 10 to 1, only 50% of the people tested positive will actually exhibit the condition.
Similarly, if only 10% of the population exhibits the condition, then in order to be 90% sure that a positive result indicates the presence of the condition, the test can give a false-positive result only 1 every 100 times it gives true positive results. Intuitively, what’s going on is that the base-10 logarithm of the “likelihood ratio” (10 and 100 respectively in the examples above) is the “number of nines added to the baseline probability”, interpreting a probability of 0.1 as having “negative 1 nine”.
The original nomogram was meant to be used with a physical ruler to do the calculations. Here, you can grab the circles and move them around to change the settings.
Casscells, Ward, Arno Schoenberger, and Thomas B Graboys. 1978. “Interpretation by Physicians of Clinical Laboratory Results.”New England Journal of Medicine 299 (18): 999–1001.
Fagan, T. J. 1975. “Nomogram for Bayes Theorem.”N Engl J Med 293 (5): 257.
---title: An Interactive Fagan Nomogrambibliography: fagan-nomogram.bibcss: fagan-nomogram/main.csscategories: [mathematics, data-science]image: /images/fagan-nomogram.jpgdate: 2020-11-14---<!-- <script src="/src/lib/d3.v5.js"></script> -->The Fagan nomogram [@letter1975nomogram] is a nomogram [@enwiki:1085193589] that computes the probabilityof the presence of some condition based on an imperfect test andvarying pre-test probabilities. It is a very handy tool to understandBayes's Theorem "physically". People typically have a sense that theless powerful the test, the less likely it is that apositive test result means the presence of the condition. Butpeople are much less likely to grasp the role of the pre-testprobability [@casscells1978interpretation].As an illustration, consider the example illustrated in the defaultsetting of the nomogram below. If only 10% of the population exhibit aparticular kind of condition, then even if a test gives a ratio oftrue positives to false positives at 10 to 1, only 50% of the peopletested positive will actually exhibit the condition.Similarly, if only 10% of the population exhibits the condition, thenin order to be 90% sure that a positive result indicates the presenceof the condition, the test can give a false-positive result only1 every 100 times it gives true positive results. Intuitively, what'sgoing on is that the base-10 logarithm of the "likelihood ratio" (10and 100 respectively in the examples above) is the "number of ninesadded to the baseline probability", interpreting a probability of 0.1as having "negative 1 nine".The original nomogram was meant to be used with a physical ruler to dothe calculations. Here, you can grab the circles and move them aroundto change the settings.<div id="main"></div><!-- <script type="module" src="fagan-nomogram/main.js"></script> -->```{ojs}import { svg } from "/src/cscheid/cscheid.js";{ svg.setupD3Prototype(d3); let svgNode = d3.select("#main") .append("svg") .attr("viewBox", "0 0 920 400") .attr("width", "100%") .attr("height", "100%"); const scaleBounds = [40, 700]; let preTestScaleLogOdds = d3.scaleLinear() .domain([3, -3]) .range(scaleBounds); let postTestScaleLogOdds = d3.scaleLinear() .domain([-3, 3]) .range(scaleBounds); let logLikelihoodRatioScale = d3.scaleLinear() .domain([-6, 6]) .range(scaleBounds); let labels = ['pre-test probability', 'likelihood ratio', 'post-test probability']; let yScale = d3.scaleLinear().domain([0,2]).range([100, 300]); svgNode.append("g") .selectAll("text") .data(labels) .enter() .append("text") .attr("x", 910) .attr("y", function(d, i) { return yScale(i); }) .attr("dy", 3) .text(function(d) { return d; }) .attr("class", "label"); let preTestLineG = svgNode.append("g"); let preTestLine = preTestLineG .append("line") .attr("x1", preTestScaleLogOdds(-3)) .attr("x2", preTestScaleLogOdds(3)) .attr("y1", 100) .attr("y2", 100) .attr("class", "test-line"); let lrLineG = svgNode.append("g"); let lrLine = lrLineG .append("line") .attr("x1", logLikelihoodRatioScale(-4)) .attr("x2", logLikelihoodRatioScale(4)) .attr("y1", 200) .attr("y2", 200) .attr("class", "test-line"); let postTestLineG = svgNode.append("g"); let postTestLine = postTestLineG .append("line") .attr("x1", postTestScaleLogOdds(-3)) .attr("x2", postTestScaleLogOdds(3)) .attr("y1", 300) .attr("y2", 300) .attr("class", "test-line"); ////////////////////////////////////////////////////////////////////////////// // this could be solved automatically.. function expFmt(v) { let l = Math.pow(10, v); let r = fmt(l / (1 + l)); return r .replace(/0+$/, "") .replace(/(\..*)09+$/, (match, p1) => `${p1}1`); // i'm so sorry, universe. } function lrFmt(v) { let l = Math.pow(10, v); if (l == ~~l) return String(l); else return String(l) .replace(/0+$/, "") .replace(/(\..*)09+$/, (match, p1) => `${p1}1`); // i'm so sorry, universe. } let preProbTicks = { list: [-3, -2, -0.954245, 0, 0.954245, 2, 3], scale: preTestScaleLogOdds, fmt: expFmt }; let postProbTicks = { list: [-3, -2, -0.954245, 0, 0.954245, 2, 3], scale: postTestScaleLogOdds, fmt: expFmt }; let lrTicks = { list: [-4, -3, -2, -1, 0, 1, 2, 3, 4], scale: logLikelihoodRatioScale, fmt: lrFmt }; let fmt = d3.format(".3f"); function addTicks(ticks) { return function(sel) { let gs = sel .enterMany(ticks.list) .append("g"); gs.append("line") .attr("x1", function(d) { return ticks.scale(d); }) .attr("x2", function(d) { return ticks.scale(d); }) .attr("y1", -5).attr("y2", 5) .attr("class", "test-line"); gs.append("text") .attr("x", function(d) { return ticks.scale(d); }) .attr("y", -15) .attr("class", "tick-label") .text(function(d) { return ticks.fmt(d); }); }; } preTestLineG .append("g") .attr("transform", svg.translate(0, 100)) .callReturn(addTicks(preProbTicks)); lrLineG .append("g") .attr("transform", svg.translate(0, 200)) .callReturn(addTicks(lrTicks)); postTestLineG .append("g") .attr("transform", svg.translate(0, 300)) .callReturn(addTicks(postProbTicks)); let nomogramLine = svgNode.append("line") .attr("x1", preTestScaleLogOdds(-1)) .attr("y1", 100) .attr("x2", postTestScaleLogOdds(0)) .attr("y2", 300) .attr("class", "test-line"); function updateLine() { nomogramLine.attr("x1", preTestHandle.attr("cx")); nomogramLine.attr("x2", postTestHandle.attr("cx")); } function dragAttrs(sel) { return sel.attr("r", 5) .attr("fill", "white") .attr("stroke", "black") .attr("cursor", "pointer"); } let preTestHandle = svgNode.append("circle") .attr("cx", preTestScaleLogOdds(-1)) .attr("cy", 100) .callReturn(dragAttrs) .call(d3.drag().on("drag", function(event) { debugger; d3.select(this).attr("cx", event.x); let lrX = Number(lrHandle.attr("cx")); let dx = lrX - event.x; postTestHandle.attr("cx", lrX + dx); updateLine(); })); let lrHandle = svgNode.append("circle") .attr("cx", 0.5 * (postTestScaleLogOdds(0) + preTestScaleLogOdds(-1))) .attr("cy", 200) .callReturn(dragAttrs) .call(d3.drag().on("drag", function(event) { debugger; d3.select(this).attr("cx", event.x); let ptX = Number(preTestHandle.attr("cx")); let dx = event.x - ptX; postTestHandle.attr("cx", event.x + dx); updateLine(); })); let postTestHandle = svgNode.append("circle") .attr("cx", postTestScaleLogOdds(0)) .attr("cy", 300) .callReturn(dragAttrs) .call(d3.drag().on("drag", function(event) { debugger; d3.select(this).attr("cx", event.x); lrHandle.attr("cx", 0.5 * (event.x + Number(preTestHandle.attr("cx")))); updateLine(); }));}```