Interactive Exploration of Diagnostic Testing

Sensitivity, specificity, postive and negative predictive values

Positive predictive value
App
Understanding how disease prevalence and test characteristics affect diagnostic test performance is crucial in clinical practice. This interactive visualisation demonstrates these relationships using a population of 100 individuals.
Author

Alasdair Warwick

Published

December 29, 2024

App

Understanding how disease prevalence and test characteristics affect diagnostic test performance is crucial in clinical practice. This interactive visualisation demonstrates these relationships using a population of 100 individuals.

Code
viewof prevalence = Inputs.range([1, 50], {
  label: "Disease Prevalence (%)",
  step: 1,
  value: 10
})

viewof sensitivity = Inputs.range([50, 100], {
  label: "Test Sensitivity (%)",
  step: 1,
  value: 80
})

viewof specificity = Inputs.range([50, 100], {
  label: "Test Specificity (%)",
  step: 1,
  value: 90
})

// Reactive calculation
grid = {
  const totalDots = 100;
  const diseaseCount = Math.round(totalDots * (prevalence / 100));
  const noDiseaseCount = totalDots - diseaseCount;

  const expectedTruePositives = Math.round(diseaseCount * (sensitivity / 100));
  const expectedFalsePositives = Math.round(noDiseaseCount * ((100 - specificity) / 100));

  // Create an array of indices for the population
  const indices = Array.from({ length: totalDots }, (_, index) => index);

  // Shuffle the indices randomly
  const shuffledIndices = indices.sort(() => Math.random() - 0.5);

  // Assign disease status based on the first `diseaseCount` indices
  const hasDisease = new Set(shuffledIndices.slice(0, diseaseCount));

  // Separate true positives and false positives
  const truePositiveIndices = Array.from(hasDisease).slice(0, expectedTruePositives);
  const falsePositiveIndices = shuffledIndices
    .filter(index => !hasDisease.has(index))
    .slice(0, expectedFalsePositives);

  // Create the dots array with proper properties
  const dots = Array(totalDots).fill(null).map((_, index) => ({
    hasDisease: hasDisease.has(index),
    testPositive: truePositiveIndices.includes(index) || falsePositiveIndices.includes(index),
  }));
  
  // Calculate statistics
  const truePositives = dots.filter(d => d.hasDisease && d.testPositive).length;
  const falsePositives = dots.filter(d => !d.hasDisease && d.testPositive).length;
  const trueNegatives = dots.filter(d => !d.hasDisease && !d.testPositive).length;
  const falseNegatives = dots.filter(d => d.hasDisease && !d.testPositive).length;

  const ppv = (truePositives / (truePositives + falsePositives)) * 100;
  const npv = (trueNegatives / (trueNegatives + falseNegatives)) * 100;
  const LRplus = (sensitivity / (100 - specificity));
  const LRminus = ((100 - sensitivity) / specificity);

  return {
    dots: dots.sort((a, b) => (b.hasDisease * 2 + b.testPositive) - (a.hasDisease * 2 + a.testPositive)),
    stats: { truePositives, falsePositives, trueNegatives, falseNegatives, ppv, npv, LRplus, LRminus }
  };
}

// Display output
display = html`
<div class="p-4 bg-white rounded-lg shadow">
  <div class="grid grid-cols-10 gap-1 p-4 bg-gray-100 rounded">
    ${grid.dots.map(dot => `
      <div class="w-6 h-6 rounded-full ${dot.hasDisease ? 'bg-red-500' : 'bg-green-400'} 
           ${dot.testPositive ? 'border-2 border-blue-500' : 'border border-transparent'}">
      </div>
    `).join('')}
  </div>
  
  <div class="grid grid-cols-2 gap-4 mt-4 p-4 bg-gray-100 rounded">
    <div>
      <p class="font-medium">Test Results:</p>
      <p>True Positives: ${grid.stats.truePositives}</p>
      <p>False Positives: ${grid.stats.falsePositives}</p>
      <p>True Negatives: ${grid.stats.trueNegatives}</p>
      <p>False Negatives: ${grid.stats.falseNegatives}</p>
    </div>
    <div>
      <p class="font-medium">Predictive Values:</p>
      <p class="font-bold">PPV: ${grid.stats.ppv.toFixed(1)}%</p>
      <p class="font-bold">NPV: ${grid.stats.npv.toFixed(1)}%</p>
      <p class="font-medium mt-4">Likelihood Ratios:</p>
      <p>LR+ = ${grid.stats.LRplus.toFixed(2)}</p>
      <p>LR- = ${grid.stats.LRminus.toFixed(2)}</p>
    </div>
  </div>
</div>
`

How to Use This Visualisation

  • Disease Prevalence: Adjust this slider to see how the proportion of diseased individuals in the population affects test performance.
  • Test Sensitivity: This represents the test’s ability to correctly identify diseased individuals.
  • Test Specificity: This shows the test’s ability to correctly identify healthy individuals.

Visual Guide

  • Each dot represents one individual in the population
  • Red dots: Individuals with disease
  • Green dots: Healthy individuals
  • Blue border: Positive test result
  • No border: Negative test result

Key Concepts

  1. Positive Predictive Value (PPV) is the probability that a person with a positive test result actually has the disease. Notice how PPV:

    • Increases with higher disease prevalence
    • Improves with better test specificity
    • Can be surprisingly low when disease prevalence is low, even with good sensitivity
  2. Negative Predictive Value (NPV) is the probability that a person with a negative test result is truly disease-free. Observe how NPV:

    • Decreases with higher disease prevalence
    • Is strongly influenced by test sensitivity
    • Tends to be high when disease prevalence is low
  3. Likelihood Ratios (LR) help interpret test results:

    • Positive Likelihood Ratio (LR+) = Sensitivity / (1 - Specificity)
      • Shows how much more likely a positive test is in diseased vs healthy people
      • LR+ > 10 provides strong evidence for disease
      • Higher values mean positive results are more meaningful
    • Negative Likelihood Ratio (LR-) = (1 - Sensitivity) / Specificity
      • Shows how much less likely a negative test is in diseased vs healthy people
      • LR- < 0.1 provides strong evidence against disease
      • Lower values mean negative results are more meaningful
Try This

Try this:

  1. Set high sensitivity (95%) and specificity (95%) to see strong LRs
  2. Notice how poor specificity drastically reduces LR+
  3. See how poor sensitivity affects LR-

Try adjusting the sliders to explore these relationships. For example:

  1. Set a very low prevalence (1%) and observe the PPV
  2. Increase specificity and watch how it affects false positives
  3. Compare PPV and NPV at different prevalence levels