*! version 2.0.1 21feb2021 daniel klein
program rioc , rclass byable(recall)
    version 11.2

    syntax varlist(numeric min=2 max=2) ///
    [ if ] [ in ] [ fweight ]           ///
    [ ,                                 ///
        Tab                             ///
        Detail                          ///
        STATs(passthru)                 ///
        CII(name)                       /// not documented
        KAPPA                           ///
        SMALLsample                     ///
        SEZERO                          ///
        ASR /*synonym*/ CHi2  /// retained; not documented
        Level(cilevel)                  ///
        CFORMAT(passthru)               ///
        PFORMAT(passthru)               ///
        SFORMAT(passthru)               ///
        PERCENT                         /// not documented
        MATCELL(name)                   /// not documented
    ]

    marksample touse

    /*
        the notation below follows Copas and Loeber (1990)

        we create 0/1 indicator variables from caller's varlist
        note: we flip the coding so that zero indicates 'true'
              and nonzero and nonmissing indicates 'false' to
              create the same table as Copas and Loeber (1990)
    */

    if ("`tab'" == "tab") tempname plbl olbl
    else               local quietly quietly

    tempvar prediction outcome
    mk_binary `prediction' `outcome' `touse' `varlist' `plbl' `olbl' , `tab'


    tempname F
    `quietly' tabulate `prediction' `outcome' [`weight' `exp'] , matcell(`F')

    if ( (r(r)!=2) | (r(c)!=2) ) {
        display as err "table not 2 by 2"
        exit 459
    }

        /*
            Copas and Loeber (1990:303) present all formulas assuming
            that e >= f; "if not, rows and columns are interchanged"
        */
    local a = `F'[1, 1]
    local b = max(`F'[1, 2], `F'[2, 1])
    local c = min(`F'[2, 1], `F'[1, 2])
    local d = `F'[2, 2]

    local e = `a' + `b'
    local f = `a' + `c'
    local n = `a' + `b' + `c' + `d'


    tempname rioc crit sr ll ul z p

        // Copas and Loeber (1990) (4)
    scalar `rioc' = (`n'*`a' - `e'*`f') / ( `f'*(`n'-`e') )

    scalar `crit' = -invnormal((1-`level'/100)/2)

    if ( mi("`smallsample'") ) {
            // Copas and Loeber (1990) (11)
        scalar `sr' = sqrt( `n'*`c'*( `n'*`f'*(`n'-`e')            ///
                    + `c'*(`n'*`e' + `e'*`f' -  2*`n'*`f' - `n'^2) ///
                    + 2*`n'*`c'^2 ) / ( (`n'-`e')^3*`f'^3 ) )
        scalar `z' = `rioc'/`sr'
        scalar `p' = 2*normal(-abs(`z'))
        scalar `ll' = `rioc' - `crit'*`sr'
        scalar `ul' = `rioc' + `crit'*`sr'
    }
    else {
        tempname alpha beta delta

        scalar `alpha' = `e'/`n'
        scalar `beta'  = `f'/`n'

            // Copas and Loeber (1990) (20)
        scalar `delta'  =  ln( ((`a'+.5)*(`d'+.5)) / ((`b'+.5)*(`c'+.5)) )

            // Copas and Loeber (1990) (21)
        scalar `sr' = sqrt( ((`e'+1)*(`e'+2)) / (`e'*(`a'+1)*(`b'+1)) ///
                    + ((`n'+1-`e')*(`n'+2-`e')) / ((`n'-`e')*(`c'+1)*(`d'+1)) )

        scalar `ll' = exp(`delta' - `crit'*`sr')
        scalar `ul' = exp(`delta' + `crit'*`sr')

            // Copas and Loeber (1990= (23)
        foreach phi in ll ul {
            scalar ``phi'' = ( 1+(``phi''-1)*(`alpha'+`beta'-2*`alpha'*`beta') ///
                           - sqrt( (1+(`alpha'+`beta')*(``phi''-1))^2          ///
                                  - 4*`alpha'*`beta'*``phi''*(``phi''-1) ) )   ///
                           / ( 2*(``phi''-1)*`beta'*(1-`alpha') )
        }
        scalar `sr' = .z
        scalar `z'  = .z
        scalar `p'  = .z
    }

    if ( ("`sezero'"=="sezero") | ("`asr'"=="asr") | ("`chi2'"=="chi2") ) {
            // Copas and Loeber (1990) (13)
        scalar `sr' = sqrt( (`e'*(`n'-`f')) / (`n'*`f'*(`n'-`e')) )
        scalar `z'  = `rioc'/`sr'
        scalar `p'  = 2*normal(-abs(`z'))
    }

    tempname table
    matrix `table' = `rioc'\ `sr'\ `z'\ `p'\ `ll'\ `ul'\ `crit'\ .\ 0
    matrix rownames `table' = b se z pvalue ll ul crit df eform
    matrix colnames `table' = RIOC // Copas and Loeber (1990) call it R


    if ( ("`detail'"=="detail") | (`"`stats'"'!="") ) {
        Detail `table' `F' `a' `b' `c' `d' `e' `f' `n' , level(`level') ///
            `smallsample' `sezero' `asr' `chi2' `detail' `stats' cii(`cii')
    }

    if ("`kappa'" == "kappa") {
        Kappa `table' `F' `e' `f' `n' , level(`level') ///
            `smallsample' `sezero' `asr' `chi2'
    }

    if ( c(noisily) ) Display `table' , nobs(`n') level(`level') ///
        `kappa' `sezero' `asr' `chi2' `cformat' `pformat' `sformat' `percent'


    if ("`matcell'" != "") matrix `matcell' = `F'

    return scalar rioc  =  `rioc'
    return scalar level = `level'
    return scalar N     =     `n'

    return local cmd      "rioc"

    return matrix table = `table'
end

program mk_binary
    syntax namelist(min=5 max=7) [ , TAB ]

    gettoken prediction namelist : namelist
    gettoken outcome    namelist : namelist
    gettoken touse      namelist : namelist

    foreach varname in `prediction' `outcome' {
        gettoken `varname' namelist : namelist
        quietly generate byte `varname' = (``varname'' == 0) if `touse'
        if ( mi("`tab'") ) continue
        local varlabel : variable label ``varname''
        if ( mi(`"`varlabel'"') ) local varlabel ``varname''
        label variable `varname' `"`varlabel'"'
    }

    if ( mi("`tab'") ) exit

    gettoken plbl olbl : namelist
    label define `plbl' 0 "True (+)" 1 "False (-)"
    label define `olbl' 0 "True"     1 "False"
    label values `prediction' `plbl'
    label values `outcome'    `olbl'
end

program Detail
    syntax anything , LEVEL(cilevel) ///
    [ SMALLSAMPLE SEZERO ASR CHI2 DETAIL STATS(passthru) CII(name) ]

    tokenize `anything'
    args table F a b c d e f n

    if ("`detail'" == "detail") {
            // Farrington and Loeber 1989 (2) (4) (5)
        local Total   `n' (`= `a' + `d'')
        local Chance  (`= `n'^2') (`= (2*`e'*`f'+`n'^2-`e'*`n'-`f'*`n')')
        local Maximum `n' (`= `f'+`n'-`e'')

        local dstats Correct:Total Correct:Chance Correct:Maximum
        local cnames `dstats'
    }

    if (`"`stats'"' != "") {
        // original table cells; orientation matters
        local b = `F'[1, 2]
        local c = `F'[2, 1]
        local e = `a' + `b'
        local f = `a' + `c'
        get_stats `a' `b' `c' `d' `e' `f' `n' , `stats' `detail'
        local i 0
        foreach sname of local snames {
            local dstats `dstats'  STAT`++i'
            local cnames `cnames' `sname'
        }
    }

    local colname : colnames `table'

    local ciicol (r(mean)\ r(se)\ .z\ .z\ r(lb)\ r(ub)\ .z\ .\ 0)
    if ( ("`sezero'"=="sezero") | ("`asr'"=="asr") | ("`chi2'"=="chi2") ) ///
        local  ciicol (r(mean)\ .z\ .z\ .z\ r(lb)\ r(ub)\ .z\ .\ 0)
    if ("`smallsample'" == "smallsample")   ///
        local  ciicol (r(mean)\ .z\ .z\ .z\  .z\ .z\ .z\ .\ 0)

    foreach s of local dstats {
        local s = substr("`s'", strpos("`s'", ":")+1, .)
        quietly cii ``s'' , level(`level') `cii'
        matrix `table' = `table', `ciicol'
    }

    matrix colnames `table' = `colname' `cnames'
end

program get_stats
    syntax anything , STATS(string asis) [ DETAIL ]

    tokenize `anything'
    args a  b  c  d  e  f  n
    args A  B  C  D  E  F  N
    args TP FP FN TN R1 C1
    args tp fp fn tn r1 c1

    local TPR a / f
    local TNR d / (n-f)
    local PPV a / e
    local NPV d / (n-e)
    local FPR b / (n-f) // not documented
    local FNR c / f     // not documented
    local ALL TPR TNR PPV NPV  // FPR FNR

    local shortcuts `ALL' FPR FNR

    local i 0
    while (`"`stats'"' != "") {
        gettoken sname stats : stats , parse(" :=")
        gettoken colon stats : stats , parse(" :=")
        if ( !inlist("`colon'", ":", "=") ) {
            local Usname = strupper("`sname'")
            local stats ``Usname'' `colon' `stats'
            if (`"`sname'"' == "all") continue
            if ( !`: list Usname in shortcuts' ) {
                display as err `"`sname' invalid"'
                error_stats
            }
            local sname `Usname'
        }
        gettoken nomin stats : stats , parse(" /") match(junk)
        gettoken slash stats : stats , parse(" /")
        gettoken denom stats : stats , parse(" ;") match(junk)

        capture noisily confirm name `sname'
        if ( _rc ) error_stats
        if ("`slash'" != "/") {
            display as err `"'`slash'' found where / expected"'
            error_stats
        }

        foreach nd in nomin denom {
            // note: must substitute the two-letter macros first
            foreach x in tp fp fn tn r1 c1 a b c d e f n {
                local `nd' : subinstr local `nd '"`x'" "``x''" , all
                local x = strupper(`"`x'"')
                local `nd' : subinstr local `nd' "`x'" "``x''" , all
            }
            capture noisily local `nd' = ``nd''
            if ( _rc ) error_stats
            capture noisily numlist "``nd''" , integer range(>0)
            if ( _rc ) error_stats
        }

        gettoken semi stats : stats , parse(";")
        if (`"`semi'"'!=";") local stats `semi' `stats'

        c_local STAT`++i' `denom' `nomin'
          local snames `snames' `sname'
    }

    c_local snames : copy local snames
end

program error_stats
    display as err "above applies to option stats()"
    exit 198
end

program Kappa
    syntax anything [ , LEVEL(cilevel) SMALLSAMPLE SEZERO ASR CHI2 ]

    tokenize `anything'
    args table F e f n

    tempname po pe kappa
    scalar `po'    = (`F'[1, 1]+`F'[2, 2])/`n'
    scalar `pe'    = (2*`e'*`f'+`n'^2-`e'*`n'-`f'*`n')/`n'^2
    scalar `kappa' = (`po'-`pe') / (1-`pe')


    // below, we follow Fleiss et al. 1969
    tempname P p1_ p2_ p_1 p_2 tc
    matrix `P'   = `F'/`n'
    scalar `p1_' = `e'/`n'
    scalar `p2_' = (`n'-`e')/`n'
    scalar `p_1' = `f'/`n'
    scalar `p_2' = (`n'-`f')/`n'
    // note that wi_ = (w1_=p_1\ w2_=p_2); w_j = (w_1=p1_, w_2=p2_)

    tempname s z p crit ll ul

    if ( mi("`smallsample'") ) {
            // Fleiss et al. 1969 [8]
        scalar `s' = sqrt( (                                         ///
                     (`P'[1, 1]*((1-`pe')-(`p_1'+`p1_')*(1-`po'))^2  ///
                   +  `P'[1, 2]*((`p_1'+`p2_')*(1-`po'))^2           ///
                   +  `P'[2, 1]*((`p_2'+`p1_')*(1-`po'))^2           ///
                   +  `P'[2, 2]*((1-`pe')-(`p_2'+`p2_')*(1-`po'))^2) ///
                   - (`po'*`pe' - 2*`pe' + `po')^2  ) / (`n'*(1-`pe')^4) )
        scalar `z'    = `kappa'/`s'
        scalar `p'    = 2*normal(-abs(`z'))
        scalar `crit' = -invnormal((1-`level'/100)/2)
        scalar `ll'   = `kappa'-`crit'*`s'
        scalar `ul'   = `kappa'+`crit'*`s'
        if ( ("`asr'"=="asr") | ("`chi2'"=="chi2") ) ///
                matrix `tc' = (`kappa'\ .z\ .z\ .z\ `ll'\ `ul'\ `crit'\ .\ 0)
        else matrix `tc' = (`kappa'\ `s'\ `z'\ `p'\ `ll'\ `ul'\ `crit'\ .\ 0)
    }
    else matrix `tc' = (`kappa'\ .z\ .z\ .z\ .z\ .z\ .z\ .\ 0)

    if ("`sezero'" == "sezero") {
            // Fleiss et al. 1969 [9]
        scalar `s' = sqrt( ///
                     (`p1_'*`p_1'*(1-(`p_1'+`p1_'))^2  ///
                   +  `p1_'*`p_2'*(0-(`p_1'+`p2_'))^2  ///
                   +  `p2_'*`p_1'*(0-(`p_2'+`p1_'))^2  ///
                   +  `p2_'*`p_2'*(1-(`p_2'+`p2_'))^2) ///
                   -  `pe'^2) / ( (1-`pe')*sqrt(`n') )
        scalar `z' = `kappa'/`s'
        scalar `p' = 2*normal(-abs(`z'))
        matrix `tc'[2, 1] = `s'
        matrix `tc'[3, 1] = `z'
        matrix `tc'[4, 1] = `p'
    }

    local colnames : colnames `table'
    matrix `table' = `table', `tc'
    matrix colnames `table' = `colnames' Kappa
end

program Display
    syntax name(name=table) , nobs(integer) LEVEL(cilevel) ///
    [ KAPPA SEZERO ASR CHI2 CFORMAT(string) PFORMAT(string) SFORMAT(string) PERCENT ]

    tempname rtable
    matrix `rtable' = (`table')'
    local nrows = rowsof(`rtable')

    if ("`percent'" == "percent") {
        local cfd %6.2f
        matrix `rtable' = ///
            `rtable'[1..., 1]*100, J(`nrows', 3, .z), ///
            `rtable'[1..., 5..6]*100, `rtable'[1..., 7..9]
    }
    else local cfd %9.0g

    set_fmt cf `cfd' `cformat'
    set_fmt pf %5.3f `pformat'
    set_fmt sf %8.2f `sformat'

    if      ( !`--nrows' ) local rspec &-
    else if ( !`--nrows' ) local rspec &--
    else {
        local and : display _dup(`--nrows') "&"
        local sep = cond("`kappa'"=="kappa", "-", "&")
        local rspec &-`and'`sep'-
    }

    local cspec & %12s | ///
        w10 `cf' & w9 `cf' o0& w8 `sf' & w6 `pf' & w11 `cf' & w10 `cf' &

    display as txt _newline "Relative improvement over chance" ///
                   _col(52) "Number of obs" _col(68) "= " as res %9.0g `nobs'

    display as txt "{hline 13}{c TT}{hline 64}"
    if ( (("`asr'"=="asr")|("`chi2'"=="chi2")) & mi("`sezero'") ) ///
    display as txt _col(14) "{c |}" _col(32) "ASR"
    display as txt _col(14) "{c |}"     ///
                   _col(21) "Coef."     ///
                   _col(29) "Std. Err." ///
                   _col(44) "z"         ///
                   _col(49) "P>|z|"     ///
                   _col(`= 61-strlen("`level'")') "[`level'% Conf. Interval]"
    display as txt "{hline 13}{c +}{hline 64}" _continue
    matlist `rtable'[., 1..6] , rspec(`rspec') cspec(`cspec') ///
        names(row) nodotz underscore
end

program set_fmt
    args f w ww
    c_local `f' `w'
    if ( mi("`ww'") ) exit
    if (fmtwidth(`"`ww'"') <= fmtwidth("`w'")) c_local `f' `ww'
    else display as txt "note: invalid `f'ormat(), using default"
end
exit

/* --------------------------------------
2.0.1 21feb2021 make binary indicators byte
                typo in a comment
2.0.0 02feb2021 changed default standard error for kappa
                confindence intervals for kappa
                new option -sezero-
                set se, z, and p to .z for -smallsample-
                -quietly- skips Display routine
                options -asr- and -chi2- no longer documented
                revised help file
1.1.0 29jan2021 support -by- (recall)
                new option -kappa-
                new option -matcell()-; not documented
                add FPR and FNR to stats(); not documented
                modified table header
                revised help file
1.0.0 26jan2021 new options -detail-, -stats()-, -{c s p}format()-
                new option -cii()-; not documented
                new option -percent-; not documented
                additional r(N), r(level), r(cmd)
                split main code into subroutines
                use locals to hold integers
                new immediate command -rioci-
                new help files
0.0.9 23jan2021 posted on Statalist