Compare two tests
This utility can be used to compare the performance of two tests evaluated in the same population. Kappa and proportional agreement for positive and negative results are calculated, as well as McNemar's Chi-squared value. Corresponding p-values are also calculated for both kappa and Chi-squared values. For infected populations, sensitivity values are calculated for both tests, as well as sensitivity covariances. Similarly, if the population is specified as uninfected, specificity values and specificity covariances are calculated.
See also:
- Fleiss JL, 1981. Statistical methods for rates and proportions, 2nd Ed. John Wiley and Sons, New York. Chapter 13.
- Cicchetti DV, Feinstein AR, 1990. High agreement but low kappa: II. Resolving the paradoxes. J. Clin. Epidemiol. 43: 551-558, for details of kappa(pos) and kappa(neg); and
- Gardner IA, Stryhn H, Lind P, Collins MT, 2000. Conditional dependence between tests affects the diagnosis and surveillance of animal diseases. Prev. Vet. Med. 45: 107-122 for calculation and application of test covariances.
No results
############################################################# # Program to compare 2 tests ############################################################# # check version and load header script rm(list = ls()) test<- ifelse(length(commandArgs()) < 3, TRUE, FALSE) fpath<- ifelse(test, "webRootUrl", "rtoolsPath") # load header scripts source(paste(fpath, "R/epi_head.R", sep = "")) source(paste(fpath, "R/HTMLStream.R", sep = "")) source(paste(fpath, "R/epitools_functions.r", sep = "")) # extract command arguments a1<- a0[8:15] # cat(a0) # cat(a1) digits<- 4 # initialise variables test1<- a1[1] test2<- a1[2] test.data<- type.convert(a1[3:6]) conf<- type.convert(a1[7]) z.conf<- qnorm(1-(1-conf)/2) names(a1)<- c("Test 1", "Test 2", "Cell a (++)", "Cell b (-+)", "Cell c (+-)", "Cell d (--)", "Confidence level") n<- sum(test.data) t1.pos<- test.data[1] + test.data[3] t1.neg<- test.data[2] + test.data[4] t2.pos<- test.data[1] + test.data[2] t2.neg<- test.data[4] + test.data[3] Pop.status<- type.convert(a1[8]) # status of source population 1 = unknown/mixed, 2 = all infected, 3 = all uninfected p<- matrix(test.data, nrow = 2, byrow = T)/n # table of inputs inputs<- matrix(type.convert(a1[3:6]), nrow = 2, ncol = 2, byrow = T) inputs<- addmargins(inputs) colnames(inputs)<- paste(a1[1], c("+ve", "-ve", "Total")) rownames(inputs)<- paste(a1[2], c("+ve", "-ve", "Total")) # Calculate kappa values obsAgree<- (test.data[1] + test.data[4])/n chanceAgree.pos<- (t2.pos/n)*(t1.pos/n) chanceAgree.neg<- (t2.neg/n)*(t1.neg/n) chanceAgree.total<- chanceAgree.pos + chanceAgree.neg obsAgree.excess<- obsAgree - chanceAgree.total maxAgree.excess<- 1 - chanceAgree.total Kappa<- obsAgree.excess/maxAgree.excess k.pos<- 2*test.data[1]/(2*test.data[1] + test.data[2] + test.data[3]) k.neg<- 2*test.data[4]/(2*test.data[4] + test.data[2] + test.data[3]) McNemar<- ((abs(test.data[2] - test.data[3]) - 1)^2)/(test.data[2] + test.data[3]) p.val.McNemar<- 1 - pchisq(McNemar, 1) abs.diff.propn<- (test.data[2] - test.data[3])/n se.abs.diff<- (sqrt((test.data[1] + test.data[4])*(test.data[2] + test.data[3]) + 4*test.data[2] * test.data[3]))/(n*sqrt(n)) rel.diff.propn<- (test.data[2] - test.data[3])/(test.data[2] + test.data[4]) se.rel.diff<- (1/(t1.neg^2))*(sqrt((n - test.data[1])*(test.data[2]*test.data[3] + t1.neg + t2.neg) - test.data[2]*test.data[3]*test.data[4])) p1<- t2.pos/n p.1<- t1.pos/n p2<- t2.neg/n p.2<- t1.neg/n temp1<- 1/((1 - chanceAgree.total)*sqrt(n)) temp2<- sqrt(chanceAgree.total + chanceAgree.total^2 - (p1*p.1*(p1 + p.1) + p2*p.2*(p2 + p.2))) se.kappa<- temp1*temp2 z.kappa<- abs(Kappa/se.kappa) p.kappa<- 1 - pnorm(z.kappa) A<- 0 for (i in 1:2) A<- A + p[i,i]*(1 - (sum(p[i,]) + sum(p[,i])) * (1 - Kappa))^2 B<- (1 - Kappa)^2 * (p[1, 2]*(sum(p[,1]) + sum(p[2,]))^2 + p[2, 1]*(sum(p[,2]) + sum(p[1,]))^2) C<- (Kappa - chanceAgree.total*(1 - Kappa))^2 se.kappa.obs<- sqrt(A+B-C)/((1 - chanceAgree.total)*sqrt(n)) lc.kappa<- Kappa - z.conf * se.kappa.obs uc.kappa<- Kappa + z.conf * se.kappa.obs lc.abs.diff<- abs.diff.propn - z.conf*se.abs.diff uc.abs.diff<- abs.diff.propn + z.conf*se.abs.diff lc.rel.diff<- rel.diff.propn - z.conf*se.rel.diff uc.rel.diff<- rel.diff.propn + z.conf*se.rel.diff results<- array("", dim = c(14, 1)) results[1]<- round(Kappa, digits) results[2]<- round(se.kappa, digits) results[3]<- round(z.kappa, 2) results[4]<- format(round(p.kappa, digits), scientific = F) results[5]<- round(k.pos, digits) results[6]<- round(k.neg, digits) results[7]<- round(obsAgree, digits) results[8]<- round(McNemar, digits) results[9]<- round(p.val.McNemar, 2) results[10]<- round(abs.diff.propn, digits) results[11]<- round(rel.diff.propn, digits) results[12]<- round(se.kappa.obs, digits) results[13]<- round(lc.kappa, digits) results[14]<- round(uc.kappa, digits) # results[14]<- lc.abs.diff # results[15]<- uc.abs.diff # results[16]<- lc.rel.diff # results[17]<- uc.rel.diff dim(results)<- c(length(results), 1) colnames(results)<- "Value" rownames(results)<- c("Kappa", "SE for kappa = 0", "Z(kappa)", "p(kappa) - one-tailed", "Proportion positive agreement", "Proportion negative agreement", "Overall proportion agreement", "McNemar's Chi sq", "p(Chi sq)", "Absolute diff. in proportions", "Relative diff. in proportions", "SE for non-zero kappa", paste("Kappa lower ", conf*100, "% limit", sep = ""), paste("Kappa upper ", conf*100, "% limit", sep = "")) # "Absolute diff. lower 95% limit", "Absolute diff. upper 95% limit", # "Relative diff. lower 95% limit", "Relative diff. upper 95% limit") #results<- round(results, digits) ###################################### # For infected populations only ###################################### # if the saqmple population are all infected!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Se.1<- t1.pos/n # sensitivity of test 1 Se.2<- t2.pos/n # sensitivity of test 1 p.2pos<- test.data[1]/n # proportion positive to both tests Se.cov<- p.2pos - Se.1*Se.2 # covariance of sensitivities Se.cov.min<- max(-1*(1 - Se.1)*(1 - Se.2), -1*Se.1*Se.2) Se.cov.max<- min(Se.1*(1 - Se.2), Se.2*(1 - Se.1)) Se.cov.propn<- ifelse(Se.cov.max == 0, NA, Se.cov/Se.cov.max) Se.results<- array(0, dim = c(7, 1)) Se.results[1]<- Se.1 Se.results[2]<- Se.2 Se.results[3]<- p.2pos Se.results[4]<- Se.cov Se.results[5]<- Se.cov.min Se.results[6]<- Se.cov.max Se.results[7]<- Se.cov.propn rownames(Se.results)<- c(paste(test1, "sensitivity"), paste(test2, "sensitivity"), "Proportion ++", "Sensitivity covariance", "Min. sensitivity covariance", "Max. sensitivity covariance", "Se covariance proportion") Se.results<- round(Se.results, digits) ###################################### # For uninfected populations only ###################################### # if the sample population are all uninfected!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Sp.1<- t1.neg/n # specificity of test 1 Sp.2<- t2.neg/n # specificity of test 1 p.2neg<- test.data[4]/n # proportion negative to both tests Sp.cov<- p.2neg - Sp.1*Sp.2 # covariance of sensitivities Sp.cov.min<- max(-1*(1 - Sp.1)*(1 - Sp.2), -1*Sp.1*Sp.2) Sp.cov.max<- min(Sp.1*(1 - Sp.2), Sp.2*(1 - Sp.1)) Sp.cov.propn<- ifelse(Sp.cov.max == 0, NA, Sp.cov/Sp.cov.max) Sp.results<- array(0, dim = c(7, 1)) Sp.results[1]<- Sp.1 Sp.results[2]<- Sp.2 Sp.results[3]<- p.2neg Sp.results[4]<- Sp.cov Sp.results[5]<- Sp.cov.min Sp.results[6]<- Sp.cov.max Sp.results[7]<- Sp.cov.propn rownames(Sp.results)<- c(paste(test1, "specificity"), paste(test2, "specificity"), "Proportion --", "Specificity covariance", "Min. specificity covariance", "Max. specificity covariance", "Sp covariance proportion") Sp.results<- round(Sp.results, digits) # write to html and file # cat(test) heading<- "Compare two tests" subheadings<- "Test comparison statistics" filename<- digest(Sys.time) tmp.file<- paste(fpath, "tmp/", filename, sep = "") result<- list(results) if (Pop.status == 2) { result[[2]]<- Se.results subheadings[2]<- "Test sensitivities and covariance assuming an infected population" } else if (Pop.status == 3) { result[[2]]<- Sp.results subheadings[2]<- "Test specificities and covariance assuming an uninfected population" } result.txt<- paste("The table below summarises relevant parameters for the comparison of two tests.
", sep = "") output<- html.output(heading, subheadings, inputs, results = result, graphs = "", graph.headings = "", show.inputs = T, show.graphs = F, tmp.file, result.txt = result.txt) write.html(output, tmp.file) cat(output)
", "Population status = ", ifelse(Pop.status == 1, "Unknown/Mixed", ifelse(Pop.status == 2, "All Infected", "All Uninfected")), "