# Matched Case-Control Logistic Regression

    Sys.setenv(OMP_THREAD_LIMIT = 1) # Reducing core use, to avoid accidental use of too many cores
    library(Colossus)
    library(data.table)
    if (system.file(package = "survival") != "") {
      library(survival)
    }
    library(dplyr)

## Matched Case-Control Modeling

One alternative regression method in Colossus is matched case-control logistic regression. The theory is presented in the following section.

### General Theory

#### Conditional Logistic Regression

Suppose we have matched case-control data and divide our data into each matched set. Each set has \(m\) cases and \(n\) records. We denote the relative risk for individual \(i\) in the set by \(r_i\). We can calculate the probability of case exposures conditional on all exposures in the set by taking the ratio of the product of relative risks in the cases to the sum of the product of relative risks for every way of selecting \(m\) individuals from the \(n\) at risk.

\[ \begin{aligned} \frac{\prod_{i}^{m} r_i}{\sum_{c \in R} \left ( \prod_{j=1}^{m} r_{c_j} \right )} \\ L = \sum_{i=1}^{m} log(r_i) - log \left ( \sum_{c \in R} \left ( \prod_{j=1}^{m} r_{c_j} \right ) \right ) \end{aligned} \]

Using the methods presented in Gail et al. (1981) we can calculate the combination of all \(n!/m!(n-m)!\) ways to select \(m\) items with a more manageable recursive formula \(B(m,n)\).

\[ \begin{aligned} B(m, n) = \sum_{c \in R} \left ( \prod_{j=1}^{m} r_{c_j} \right ) \\ B(m,n) = B(m, n-1) + r_n B(m-1, n-1) \\ B(m,n) = \begin{cases} \sum_{j}^{n} r_j & m = 1 \\ 0 & m > n \end{cases} \end{aligned} \]

We can then directly solve for the first and second derivatives and their recursive formula.

\[ \begin{aligned} \frac{\partial r_i}{\partial \beta_\mu} =: r_{i}^{\mu} \\ \frac{\partial B(m,n)}{\partial \beta_\mu} = B^{\mu}(m, n) = \sum_{c \in R} \left [ \left ( \sum_{j=1}^{m} \frac{r_{c_j}^{\mu}}{r_{c_j}} \right ) \prod_{j=1}^{m} r_{c_j} \right ] \\ B^{\mu}(m,n) = B^{\mu}(m, n-1) + r_n B^{\mu}(m-1, n-1) + r_n^{\mu} B(m-1, n-1) \\ B^{\mu}(m,n) = \begin{cases} \sum_{j}^{n} r_j^{\mu} & m = 1 \\ 0 & m > n \end{cases} \end{aligned} \]

\[ \begin{aligned} \frac{\partial^2 r_i}{\partial \beta_\mu \partial \beta_\nu} =: r_{i}^{\mu,\nu} \\ \frac{\partial^2 B(m,n)}{\partial \beta_\mu \partial \beta_\nu} = B^{\mu,\nu}(m, n) = \sum_{c \in R} \left [ \left ( \sum_{j=1}^{m} \frac{r_{c_j}^{\mu,\nu}}{r_{c_j}} + \left ( \sum_{j=1}^{m} \frac{r_{c_j}^{\mu}}{r_{c_j}} \right ) \left ( \sum_{j=1}^{m} \frac{r_{c_j}^{\nu}}{r_{c_j}} \right ) - \sum_{j=1}^{m} \frac{r_{c_j}^{\mu}}{r_{c_j}} \frac{r_{c_j}^{\nu}}{r_{c_j}} \right ) \prod_{j=1}^{m} r_{c_j} \right ] \\ B^{\mu,\nu}(m,n) = B^{\mu,\nu}(m, n-1) + r_n^{\mu,\nu} B(m-1, n-1) + r_n^{\nu} B^{\mu}(m-1, n-1) + r_n^{\mu} B^{\nu}(m-1, n-1) + r_n B^{\mu,\nu}(m-1, n-1) \\ B^{\mu,\nu}(m,n) = \begin{cases} \sum_{j}^{n} r_j^{\mu,\nu} & m = 1 \\ 0 & m > n \end{cases} \end{aligned} \]

Finally, these expressions for \(B(m,n)\) can be substituted into the equations for the contribution of Log-Likelihood and its derivatives from each matched set. The model is then optimized via the same methods as the other regression models.

\[ \begin{aligned} L_{set} = \sum_{i=1}^{m} log(r_i) - log \left ( B(m,n) \right ) \\ L^{\mu}_{set} = \sum_{i=1}^{m} \frac{r_i^{\mu}}{r_i} - \frac{B^{\mu}(m,n)}{B(m,n)} \\ L^{\mu, \nu}_{set} = \sum_{i=1}^{m} \left ( \frac{r_i^{\mu,\nu}}{r_i} - \frac{r_i^{\mu}}{r_i}\frac{r_i^{nu}}{r_i} \right ) - \left ( \frac{B^{\mu,\nu}(m,n)}{B(m,n)} - \frac{B^{\mu}(m,n)}{B(m,n)}\frac{B^{\nu}(m,n)}{B(m,n)} \right ) \end{aligned} \]

#### Unconditional Logistic Regression

It is important to note that the recursive formula calculation can quickly become time-consuming, particularly if there is a large number of cases. To make the matched case-control method generally applicable, the likelihood function can be changed to a logistic regression model in matched sets with a large number of cases. In general, the matched case-control regression function adds an item to the model control list, “cond_thres”, to set the threshold to switch to a logistic regression model.

The logistic loglikelihood is defined by treating the matched case-control data as single trial data. The likelihood is a function of the event status (\(\theta_i\)) and odds ratio (\(O_i\)). The odds ratio for any row is calculated as the product of the odds ratio for the matched set (\(O_s\)) and the relative risk for the row (\(r_i\)). Behind the scenes, Colossus optimizes both the model parameters (\(\vec{\beta}\)) for the relative risk as well as a logistic model for the matched set odds ratios (\(O_s=e^{\alpha_s}\)).

\[ \begin{aligned} L_i = y_i ln(O_s r_i) - ln(1+ O_s r_i) \\ \frac{\partial L_i}{\partial \beta_j} = y_i \frac{r^j_i}{r_i} - O_s \frac{r^j_i}{1 + O_s r_i}\\ \frac{\partial L_i}{\partial \alpha_s} = (y_i - 1) + \frac{1}{1 + O_s r_i} \\ \frac{\partial^2 L_i}{\partial \beta_j \partial \beta_k} = y_i \left ( \frac{r^{j,k}_i}{r_i} - \frac{r^j_i}{r_i} \frac{r^k_i}{r_i} \right ) - O_s \left ( \frac{r^{j,k}_i}{1 + O_s r_i} - O_s \frac{r^j_i}{1 + O_s r_i} \frac{r^k_i}{1 + O_s r_i} \right ) \\ \frac{\partial^2 L_i}{\partial \beta_j \partial \alpha_s} = \frac{-O_s r^j_i}{1 + O_s r_i} \\ \frac{\partial^2 L_i}{\partial \alpha_s^2} = \frac{-O_s r_i}{1 + O_s r_i} \end{aligned} \]

### Examples of Use

The following provides a basic example of how to use the matched case-control regression function. The data used is from a lung cancer study, included in the R survival library. Slight adjustments were made to make the data line up with data included with 32-bit Epicure, for comparison. In short, we want to model the effects of treatment and Karnofsky performance score on sets matched by cancer cell type.

    #
    if (system.file(package = "survival") != "") {
      data(cancer, package = "survival")
      veteran |> setDT()
      df <- copy(veteran)
    } else {
      karno <- c(60, 70, 60, 60, 70, 20, 40, 80, 50, 70, 60, 40, 30, 80, 70, 60, 60, 40, 80, 60, 40, 60, 60, 30, 80, 30, 50, 60, 80, 40, 20, 80, 30, 75, 70, 60, 30, 60, 80, 60, 70, 50, 50, 40, 40, 20, 70, 40, 80, 80, 50, 80, 30, 80, 50, 80, 50, 70, 60, 40, 80, 80, 70, 90, 90, 80, 80, 70, 60, 90, 80, 80, 50, 50, 70, 70, 20, 60, 90, 30, 20, 70, 90, 80, 50, 70, 60, 90, 50, 30, 70, 20, 30, 60, 40, 30, 20, 60, 70, 80, 85, 70, 70, 70, 50, 30, 40, 40, 40, 99, 80, 60, 60, 60, 60, 50, 70, 10, 40, 70, 90, 80, 50, 40, 40, 60, 70, 30, 60, 30, 60, 80, 75, 60, 70, 80, 30)
      trt <- c(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2)
      cell <- c("squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "large", "large", "large", "large", "large", "large", "large", "large", "large", "large", "large", "large", "large", "large", "large", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "squamous", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "smallcell", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "adeno", "large", "large", "large", "large", "large", "large", "large", "large", "large", "large", "large", "large")
      status <- c(1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)

      df <- data.table(
        karno = karno,
        trt = trt,
        cell = cell,
        status = status
      )
    }
    # Make the same adjustments as Epicure example 6.5
    karno <- df$karno
    karno[93] <- 20
    df$karno <- karno
    df$trt <- df$trt - 1
    df$trt <- as.integer(df$trt == 0)
    cell_lvl <- c("large", "squamous", "smallcell", "adeno")
    df$cell <- as.integer(factor(df$celltype, level = cell_lvl)) - 1

    df$karno50 <- df$karno - 50

In the following examples, we are using matching by strata and changing the conditional threshold. In the first case, every matched set uses the recursive formula. In the second case, one set uses the simplified formula. Finally in the third case, every set uses the simplified formula. In all cases, the regression returns the typical output and can be summarized similarly to other regression function outputs.

    model <- CaseControl_Strata(status, cell) ~ loglinear(karno50, trt)

    control <- list(verbose = 0, maxiters = c(1, 6), ncores = 1)
    e0 <- CaseControlRun(model, df, control = control, conditional_threshold = 100)
    e1 <- CaseControlRun(model, df, control = control, conditional_threshold = 40)
    e2 <- CaseControlRun(model, df, control = control, conditional_threshold = 0)

    print(e0)
    #> |--------------------------------------------------------------------------------|
    #> Final Results
    #>    Covariate Subterm Central Estimate Standard Error 2-tail p-value
    #>       <char>  <char>            <num>          <num>          <num>
    #> 1:   karno50  loglin          -0.0435         0.0232         0.0605
    #> 2:       trt  loglin          -0.3658         0.7370         0.6197
    #> |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -|
    #>
    #> Matched Case-Control Model Used
    #> Multiplicative-Excess Model Used: T0*(1+T1)*(1+T2)*...
    #> Model stratified by 'cell'
    #> |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -|
    #> Deviance: 49.859
    #> 0 out of 4 matched sets used Unconditional Likelihood
    #> Iterations run: 5
    #> maximum step size: 1.205e-03, maximum first derivative: 7.387e-02
    #> Last iteration improved the log-likelihood by: 2.357e-05
    #> Analysis converged
    #> Records Used: 137, Records Removed: 0
    #> Run finished in 0.065 seconds
    #> |--------------------------------------------------------------------------------|
    print(e1)
    #> |--------------------------------------------------------------------------------|
    #> Final Results
    #>    Covariate Subterm Central Estimate Standard Error 2-tail p-value
    #>       <char>  <char>            <num>          <num>          <num>
    #> 1:   karno50  loglin          -0.0438         0.0205         0.0324
    #> 2:       trt  loglin          -0.3699         0.5734         0.5188
    #> |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -|
    #>
    #> Matched Case-Control Model Used
    #> Multiplicative-Excess Model Used: T0*(1+T1)*(1+T2)*...
    #> Model stratified by 'cell'
    #> |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -|
    #> Deviance: 52.752
    #> 1 out of 4 matched sets used Unconditional Likelihood
    #> Iterations run: 5
    #> maximum step size: 1.505e-03, maximum first derivative: 6.948e-02
    #> Last iteration improved the log-likelihood by: 3.852e-05
    #> Analysis converged
    #> Records Used: 137, Records Removed: 0
    #> Run finished in 0.084 seconds
    #> |--------------------------------------------------------------------------------|
    print(e2)
    #> |--------------------------------------------------------------------------------|
    #> Final Results
    #>    Covariate Subterm Central Estimate Standard Error 2-tail p-value
    #>       <char>  <char>            <num>          <num>          <num>
    #> 1:   karno50  loglin          -0.0448         0.0163        0.00608
    #> 2:       trt  loglin          -0.3767         0.5567        0.49856
    #> |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -|
    #>
    #> Matched Case-Control Model Used
    #> Multiplicative-Excess Model Used: T0*(1+T1)*(1+T2)*...
    #> Model stratified by 'cell'
    #> |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -|
    #> Deviance: 59.723
    #> 4 out of 4 matched sets used Unconditional Likelihood
    #> Iterations run: 5
    #> maximum step size: 1.872e-03, maximum first derivative: 3.469e-02
    #> Last iteration improved the log-likelihood by: 6.414e-05
    #> Analysis converged
    #> Records Used: 137, Records Removed: 0
    #> Run finished in 0.062 seconds
    #> |--------------------------------------------------------------------------------|