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
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
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
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:
Set high sensitivity (95%) and specificity (95%) to see strong LRs
Notice how poor specificity drastically reduces LR+
See how poor sensitivity affects LR-
Try adjusting the sliders to explore these relationships. For example:
Set a very low prevalence (1%) and observe the PPV
Increase specificity and watch how it affects false positives
Compare PPV and NPV at different prevalence levels