Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions R/robincar-contrast.R
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,30 @@ diff <- function(est){
return(cont)
}

odds_ratio <- function(est){
base <- est[1] / (1-est[1])
cont <- est[-1] / (1-est[-1]) / base
return(cont)
}

log_odds_ratio <- function(est){
base <- est[1] / (1-est[1])
cont <- log(est[-1] / (1-est[-1]) / base)
return(cont)
}

ratio <- function(est){
base <- est[1]
cont <- est[-1] / base
return(cont)
}

log_ratio <- function(est){
base <- est[1]
cont <- log(est[-1] / base)
return(cont)
}

jacobian <- function (settings, est) {
UseMethod("jacobian", settings)
}
Expand All @@ -30,6 +48,30 @@ jacobian.RATIO <- function(settings, est){
return(jacob)
}

#' @exportS3Method
jacobian.LOGRATIO <- function(settings, est){
col1 <- -1 / est[1]
mat1 <- diag(1/est[-1], length(est)-1)
jacob <- cbind(col1, mat1)
return(jacob)
}

#' @exportS3Method
jacobian.ODDSRATIO <- function(settings, est){
col1 <- -1 / est[1] ** 2 * est[-1] / (1-est[-1])
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The complex mathematical expression should be broken down or commented for clarity. Consider splitting into multiple lines or adding a comment explaining the mathematical formula being implemented.

Suggested change
col1 <- -1 / est[1] ** 2 * est[-1] / (1-est[-1])
# Compute the first column of the Jacobian for the odds ratio transformation.
# Formula: -1 / (est[1]^2) * est[-1] / (1 - est[-1])
base_sq <- est[1]^2
odds_cont <- est[-1] / (1 - est[-1])
col1 <- -1 / base_sq * odds_cont

Copilot uses AI. Check for mistakes.
mat1 <- diag((1-est[-1]) / est[-1] / (1-est[1]) ** 2, length(est)-1)
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The complex mathematical expression should be broken down or commented for clarity. Consider splitting into multiple lines or adding a comment explaining the mathematical formula being implemented.

Suggested change
mat1 <- diag((1-est[-1]) / est[-1] / (1-est[1]) ** 2, length(est)-1)
# mat1 is the derivative of the odds ratio with respect to each comparison group.
# It is calculated as: (1 - est[-1]) / (est[-1] * (1 - est[1])^2)
numer <- (1 - est[-1])
denom <- est[-1] * (1 - est[1])^2
mat1 <- diag(numer / denom, length(est)-1)

Copilot uses AI. Check for mistakes.
jacob <- cbind(col1, mat1)
return(jacob)
}

#' @exportS3Method
jacobian.LOGODDSRATIO <- function(settings, est){
col1 <- -1 / (est[1] * (1 - est[1]))
mat1 <- diag(1 / (est[-1] * (1 - est[-1])), length(est)-1)
jacob <- cbind(col1, mat1)
return(jacob)
}

#' @exportS3Method
jacobian.CUSTOM <- function(settings, est){
if(!is.null(settings$dh)){
Expand Down Expand Up @@ -59,6 +101,18 @@ contrast.settings <- function(k, contrast_h, contrast_dh=NULL){
h <- ratio
type <- "RATIO"
name <- function(tl) paste0("treat ", tl[-1], " / ", tl[1])
} else if(contrast_h == "log_ratio"){
h <- log_ratio
type <- "LOGRATIO"
name <- function(tl) paste0("log(treat ", tl[-1], " / ", tl[1], ")")
} else if(contrast_h == "odds_ratio"){
h <- odds_ratio
type <- "ODDSRATIO"
name <- function(tl) paste0("odds(treat ", tl[-1], ") / odds(", tl[1], ")")
} else if(contrast_h == "log_odds_ratio"){
h <- log_odds_ratio
type <- "LOGODDSRATIO"
name <- function(tl) paste0("log(odds(treat ", tl[-1], ")) - log(odds(", tl[1], "))")
} else {
stop("Unrecognized contrast function name.")
}
Expand All @@ -80,6 +134,22 @@ contrast <- function(settings, treat, est, varcov){
# Get labels
lab <- settings$name(treat)

# If using an odds ratio contrast, make sure
# give a warning if the estimates are not between 1 and 0.
if(class(settings)[2] %in% c("ODDSRATIO", "LOGODDSRATIO")){
if(any(est < 0) || any(est > 1)){
warning("Estimates are not between 0 and 1: are you sure you want an odds ratio?")
}
}

# If using a ratio or odds ratio, give a warning
# that the performance is better with logs.
if(class(settings)[2] %in% c("ODDSRATIO", "RATIO")){
warning("Using a ratio or odds ratio is not recommended.
Consider using log_ratio or log_odds_ratio for better
performance of the variance estimator.")
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning message has inconsistent indentation and formatting. The multi-line string should be properly formatted, either as a single line or with consistent indentation.

Suggested change
performance of the variance estimator.")
warning("Using a ratio or odds ratio is not recommended. Consider using log_ratio or log_odds_ratio for better performance of the variance estimator.")

Copilot uses AI. Check for mistakes.
}

# Get transformed estimate
c_est <- settings$h(est)

Expand Down
90 changes: 90 additions & 0 deletions tests/testthat/test-contrast.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,93 @@ test_that("Test contrast function ordering", {
expect_equal(as.vector(fit.anova$contrast$result$estimate[1]), anova)

})

test_that("Test warnings for ratio and odds ratio functions", {

data <- RobinCar:::data_sim %>%
# recode A from 0 to "a", 1 to "b", 2 to "c"
mutate(A=factor(A, levels=0:2, labels=c("a", "b", "c"))) %>%
mutate(y_bin=ifelse(y > mean(y), 1, 0)) # create binary outcome

warning_txt <- "Using a ratio or odds ratio is not recommended.
Consider using log_ratio or log_odds_ratio for better
performance of the variance estimator."
Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning text has inconsistent formatting with extra whitespace and indentation that doesn't match the actual warning message in the source code. This could cause test failures if the formatting doesn't exactly match.

Suggested change
performance of the variance estimator."
warning_txt <- "Using a ratio or odds ratio is not recommended. Consider using log_ratio or log_odds_ratio for better performance of the variance estimator."

Copilot uses AI. Check for mistakes.

# Test for linear model with ratio
expect_warning(
fit <- robincar_glm(
df=data,
response_col="y_bin",
treat_col="A",
formula= y_bin ~ A,
contrast_h="odds_ratio"
),
warning_txt
)

# Test for linear model with ratio
expect_warning(
fit <- robincar_glm(
df=data,
response_col="y",
treat_col="A",
formula= y ~ A,
contrast_h="ratio"
),
warning_txt
)

})



test_that("Test warnings for contrast functions that are not probabilities", {
# We want these to give warnings when someone
# specifies that they want a log odds ratio.

data <- RobinCar:::data_sim %>%
# recode A from 0 to "a", 1 to "b", 2 to "c"
mutate(A=factor(A, levels=0:2, labels=c("a", "b", "c"))) %>%
mutate(y_bin=ifelse(y > mean(y), 1, 0)) # create binary outcome

# Test for linear model with ratio
expect_warning(
fit <- robincar_glm(
df=data,
response_col="y",
treat_col="A",
formula= y ~ A,
contrast_h="log_odds_ratio"
),
"Estimates are not between 0 and 1: are you sure you want an odds ratio?"
)

})

test_that("Compare log odds ratio SEs to GLM", {
# We want these to give warnings when someone
# specifies that they want a log odds ratio.

Copy link

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is incorrect for this test. This test doesn't check for warnings - it compares log odds ratio estimates to GLM output. The comment appears to be copied from the previous test.

Suggested change
# Compare log odds ratio estimates from robincar_glm to standard GLM output.

Copilot uses AI. Check for mistakes.
data <- RobinCar:::data_sim %>%
# recode A from 0 to "a", 1 to "b", 2 to "c"
mutate(A=factor(A, levels=0:2, labels=c("a", "b", "c"))) %>%
mutate(y_bin=ifelse(y > mean(y), 1, 0)) # create binary outcome

# Test for linear model with ratio
fit <- robincar_glm(
df=data,
response_col="y_bin",
treat_col="A",
formula= y_bin ~ A,
contrast_h="log_odds_ratio"
)

or <- with(data, glm(y_bin ~ A, family=binomial(link="logit")))

# Check that log odds ratios match
expect_equal(
unname(or$coefficients[2:3]),
unname(fit$contrast$result$estimate), tolerance=1e-6)

})