Code
library(ivdtools)
library(readr)Qualitative analysis evaluates the degree of agreement between the candidate method and the reference method in positive/negative determinations, corresponding to CLSI EP12. ivdtools provides raw_to_table() to build a four-fold table from raw paired data, counts_to_table() to build one directly from existing TP/FP/TN/FN counts, and three accompanying analysis methods: diagnostics() (sensitivity, specificity, PPV, NPV, likelihood ratios, etc.), kappa() (Kappa and PABAK), and mcnemar() (paired-difference test). Quantitative results can be converted to qualitative with continuous_to_binary().
This document uses three examples to demonstrate the complete analysis for the three data sources. The example data are deterministic teaching data; the positive direction and threshold should be determined according to the protocol.
library(ivdtools)
library(readr)| Function | Main purpose | Key input or output |
|---|---|---|
raw_to_table() |
Build a four-fold table from raw paired data | candidate, reference, positive, na.rm |
counts_to_table() |
Build a four-fold table from TP/FP/TN/FN | candidate/reference naming |
continuous_to_binary() |
Convert quantitative to qualitative | cutoff (≥ cutoff is 1) |
describe() |
Description of raw data | only available for raw_to_table() sources |
diagnostics() |
Diagnostic accuracy metrics | ci.method, prevalence |
kappa() |
Kappa / PABAK | prevalence switch |
mcnemar() |
Paired-difference test | uses exact test when discordant pairs <25 |
qual <- read_csv("./data/qualitative-raw.csv", show_col_types = FALSE)
qual <- as.data.frame(qual)
str(qual)'data.frame': 60 obs. of 5 variables:
$ id : chr "S001" "S002" "S003" "S004" ...
$ new : chr "positive" "negative" "negative" "negative" ...
$ gold : chr "positive" "negative" "negative" "negative" ...
$ age : num 46.1 44.7 65.7 60 64.6 32.7 47 40.9 38.4 48.1 ...
$ gender: chr "M" "F" "M" "F" ...
table(qual$gold)
negative positive
36 24
table(qual$new)
negative positive
36 24
qa <- raw_to_table(
qual,
candidate = "new",
reference = "gold",
id = "id",
positive = "positive"
)
qa
Fourfold Table
Source : raw data
Total n : 60
Duplicate IDs : none
Candidate : new
Reference : gold
2x2 Contingency Table
----------------------------------------
gold
positive negative Sum
----------------------------------------
new positive 23 1 24
negative 1 35 36
sum 24 36 60
----------------------------------------
Parameter notes:
raw_to_table()requires that both the candidate and reference columns have exactly 2 levels;positivespecifies the positive level (used for level mapping of factor/character columns);na.rmcontrols whether missing rows are deleted.idis optional and used for duplicate-sample detection.
qa <- describe(qa)
Data Summary
Rows: 60
Columns: 5
ID: 60 unique, no duplicates
new
negative 36 ( 60.0%)
positive 24 ( 40.0%)
gold
negative 36 ( 60.0%)
positive 24 ( 40.0%)
age
Mean (SD): 51.05 (13.45)
Median (Q1-Q3): 48.90 (42.15 - 57.28)
Range: 24.30 - 90.10
gender
F 32 ( 53.3%)
M 28 ( 46.7%)
describe() is only available for raw_to_table() sources; it outputs the row count, ID duplicates, per-column summaries, and missingness.
qa <- diagnostics(qa, ci.method = "wilson")
Diagnostic Performance Summary
Sensitivity 0.958 (0.798, 0.993)
Specificity 0.972 (0.858, 0.995)
PPV 0.958 (0.798, 0.993)
NPV 0.972 (0.858, 0.995)
Accuracy 0.967 (0.886, 0.991)
Prevalence 0.400 (0.286, 0.526)
LR+ 34.500 (4.986, 238.724)
LR- 0.043 (0.006, 0.292)
Odds Ratio 805.000 (47.921, 13522.837)
# PPV & NPV are based on data prevalence of 0.400.
# Confidence intervals for proportions use 'wilson' method.
qa$diagnostics[c("sensitivity", "specificity", "ppv", "npv", "accuracy")]$sensitivity
est lower upper
0.9583333 0.7975819 0.9926065
$specificity
est lower upper
0.9722222 0.8583028 0.9950796
$ppv
est lower upper
0.9583333 0.7975819 0.9926065
$npv
est lower upper
0.9722222 0.8583028 0.9950796
$accuracy
est lower upper
0.9666667 0.8863623 0.9908107
In this example, the sensitivity is 0.958 and the specificity is 0.972. If the prevalence in the target population is known, use prevalence to recompute PPV/NPV:
qa <- diagnostics(qa, prevalence = 0.4)
Diagnostic Performance Summary
Sensitivity 0.958 (0.798, 0.993)
Specificity 0.972 (0.858, 0.995)
PPV 0.958 (0.790, 0.993)
NPV 0.972 (0.864, 0.995)
Accuracy 0.967 (0.886, 0.991)
Prevalence 0.400
LR+ 34.500 (4.986, 238.724)
LR- 0.043 (0.006, 0.292)
Odds Ratio 805.000 (47.921, 13522.837)
# PPV & NPV are based on user-specified prevalence of 0.400.
# Confidence intervals for proportions use 'wilson' method.
qa$diagnostics[c("ppv", "npv")]$ppv
est lower upper
0.9583333 0.7895852 0.9926193
$npv
est lower upper
0.9722222 0.8641373 0.9950711
Parameter notes: The
ci.methodofdiagnostics()accepts 7 proportion confidence-interval methods (default"wilson"); whenprevalenceis given, PPV/NPV are recomputed by the Bayes formula; otherwise they are based on the observed prevalence.
qa <- kappa(qa)
Cohen's Kappa
new vs gold
n = 60
Kappa 0.9306
SE 0.0483
95% CI (0.8359, 1.0000)
Observed agreement 0.9667
Expected agreement 0.5200
qa <- kappa(qa, prevalence = 0.5)
PABAK
new vs gold
n = 60
Kappa 0.9333
SE 0.0463
95% CI (0.8425, 1.0000)
Observed agreement 0.9667
Expected agreement 0.5000
# PABAK (prevalence = 0.500): 2 * p_obs - 1, assumes expected agreement = 0.5
Parameter notes:
kappa()gives Cohen’s Kappa; whenprevalenceis given, it instead computes PABAK (2·p_obs − 1). The two handle prevalence bias differently; note the method in the report.
qa <- mcnemar(qa)
McNemar Test
new vs gold
Discordant pairs: b (pos, neg) = 1, c (neg, pos) = 1
n (b + c) = 2
Method: Exact binomial test
P-value: 1.0000
Conclusion: Not significant at 95% confidence level
The discordant pairs b+c = 2 < 25, so the function automatically uses the exact binomial test.
summary(qa)
Qualitative Analysis - summary
-------------------------------------------------
Fourfold Table
Source : raw data
Total n : 60
Duplicate IDs : none
Candidate : new
Reference : gold
2x2 Contingency Table
----------------------------------------
gold
positive negative Sum
----------------------------------------
new positive 23 1 24
negative 1 35 36
sum 24 36 60
----------------------------------------
Analyses performed:
[x] Describe
[x] Diagnostics
[x] Kappa
[x] McNemar
Data Summary
Rows: 60
Columns: 5
ID: 60 unique, no duplicates
new
negative 36 ( 60.0%)
positive 24 ( 40.0%)
gold
negative 36 ( 60.0%)
positive 24 ( 40.0%)
age
Mean (SD): 51.05 (13.45)
Median (Q1-Q3): 48.90 (42.15 - 57.28)
Range: 24.30 - 90.10
gender
F 32 ( 53.3%)
M 28 ( 46.7%)
Diagnostic Performance Summary
Sensitivity 0.958 (0.798, 0.993)
Specificity 0.972 (0.858, 0.995)
PPV 0.958 (0.790, 0.993)
NPV 0.972 (0.864, 0.995)
Accuracy 0.967 (0.886, 0.991)
Prevalence 0.400
LR+ 34.500 (4.986, 238.724)
LR- 0.043 (0.006, 0.292)
Odds Ratio 805.000 (47.921, 13522.837)
# PPV & NPV are based on user-specified prevalence of 0.400.
# Confidence intervals for proportions use 'wilson' method.
PABAK
new vs gold
n = 60
Kappa 0.9333
SE 0.0463
95% CI (0.8425, 1.0000)
Observed agreement 0.9667
Expected agreement 0.5000
# PABAK (prevalence = 0.500): 2 * p_obs - 1, assumes expected agreement = 0.5
McNemar Test
new vs gold
Discordant pairs: b (pos, neg) = 1, c (neg, pos) = 1
n (b + c) = 2
Method: Exact binomial test
P-value: 1.0000
Conclusion: Not significant at 95% confidence level
When counts are already available, no raw data are needed:
tab <- counts_to_table(
tp = 80, fp = 5, tn = 120, fn = 3,
candidate = "new method",
reference = "gold standard"
)
tab
Fourfold Table
Source : counts
Total n : 208
Candidate : new method
Reference : gold standard
2x2 Contingency Table
-----------------------------------------------
gold standard
positive negative Sum
-----------------------------------------------
new method positive 80 5 85
negative 3 120 123
sum 83 125 208
-----------------------------------------------
tab <- diagnostics(tab)
Diagnostic Performance Summary
Sensitivity 0.964 (0.899, 0.988)
Specificity 0.960 (0.910, 0.983)
PPV 0.941 (0.870, 0.975)
NPV 0.976 (0.931, 0.992)
Accuracy 0.962 (0.926, 0.980)
Prevalence 0.399 (0.335, 0.467)
LR+ 24.096 (10.198, 56.934)
LR- 0.038 (0.012, 0.114)
Odds Ratio 640.000 (148.773, 2753.180)
# PPV & NPV are based on data prevalence of 0.399.
# Confidence intervals for proportions use 'wilson' method.
tab <- kappa(tab)
Cohen's Kappa
new method vs gold standard
n = 208
Kappa 0.9201
SE 0.0277
95% CI (0.8659, 0.9744)
Observed agreement 0.9615
Expected agreement 0.5184
tab <- mcnemar(tab)
McNemar Test
new method vs gold standard
Discordant pairs: b (pos, neg) = 5, c (neg, pos) = 3
n (b + c) = 8
Method: Exact binomial test
P-value: 0.7266
Conclusion: Not significant at 95% confidence level
summary(tab)
Qualitative Analysis - summary
-------------------------------------------------
Fourfold Table
Source : counts
Total n : 208
Candidate : new method
Reference : gold standard
2x2 Contingency Table
-----------------------------------------------
gold standard
positive negative Sum
-----------------------------------------------
new method positive 80 5 85
negative 3 120 123
sum 83 125 208
-----------------------------------------------
Analyses performed:
[ ] Describe
[x] Diagnostics
[x] Kappa
[x] McNemar
Diagnostic Performance Summary
Sensitivity 0.964 (0.899, 0.988)
Specificity 0.960 (0.910, 0.983)
PPV 0.941 (0.870, 0.975)
NPV 0.976 (0.931, 0.992)
Accuracy 0.962 (0.926, 0.980)
Prevalence 0.399 (0.335, 0.467)
LR+ 24.096 (10.198, 56.934)
LR- 0.038 (0.012, 0.114)
Odds Ratio 640.000 (148.773, 2753.180)
# PPV & NPV are based on data prevalence of 0.399.
# Confidence intervals for proportions use 'wilson' method.
Cohen's Kappa
new method vs gold standard
n = 208
Kappa 0.9201
SE 0.0277
95% CI (0.8659, 0.9744)
Observed agreement 0.9615
Expected agreement 0.5184
McNemar Test
new method vs gold standard
Discordant pairs: b (pos, neg) = 5, c (neg, pos) = 3
n (b + c) = 8
Method: Exact binomial test
P-value: 0.7266
Conclusion: Not significant at 95% confidence level
describe(tab) # counts sources have no raw data; describe() is unavailableObjects built by counts_to_table() have no raw data, so describe() is unavailable; diagnostics(), kappa(), and mcnemar() all work normally.
The candidate method is a quantitative result result and the reference is qualitative gold:
cont <- read_csv("./data/qualitative-continuous.csv", show_col_types = FALSE)
cont <- as.data.frame(cont)
str(cont)'data.frame': 50 obs. of 3 variables:
$ id : chr "S001" "S002" "S003" "S004" ...
$ result: num 10.11 13.06 1.53 7.78 4.98 ...
$ gold : chr "positive" "positive" "negative" "negative" ...
Convert to binary at cutoff = 8:
cont_bin <- continuous_to_binary(cont, cols = "result", cutoff = 8)
head(cont_bin)continuous_to_binary() generates a result_binary column (≥ cutoff is 1, otherwise 0). To satisfy the positive mapping requirement of raw_to_table(), first relabel it with labels consistent with the reference:
cont_bin$result_binary_lab <- ifelse(cont_bin$result_binary == 1, "positive", "negative")qa3 <- raw_to_table(
cont_bin,
candidate = "result_binary_lab",
reference = "gold",
id = "id",
positive = "positive"
)
qa3 <- diagnostics(qa3)
Diagnostic Performance Summary
Sensitivity 0.952 (0.773, 0.992)
Specificity 0.897 (0.736, 0.964)
PPV 0.870 (0.679, 0.955)
NPV 0.963 (0.817, 0.993)
Accuracy 0.920 (0.812, 0.968)
Prevalence 0.420 (0.294, 0.558)
LR+ 9.206 (3.140, 26.994)
LR- 0.053 (0.008, 0.361)
Odds Ratio 173.333 (16.746, 1794.100)
# PPV & NPV are based on data prevalence of 0.420.
# Confidence intervals for proportions use 'wilson' method.
qa3$diagnostics[c("sensitivity", "specificity", "accuracy")]$sensitivity
est lower upper
0.9523810 0.7733064 0.9915440
$specificity
est lower upper
0.8965517 0.7361492 0.9641851
$accuracy
est lower upper
0.9200000 0.8116175 0.9684505
Note: The level specified by
positivemust exist in both columns, so first relabel the binary column aspositive/negative. Ifpositiveis not specified, the function may reverse the positive/negative direction according to the sorted levels, flipping TP/FP; be sure to check.