Skip to content

Commit

Permalink
Reduce n_pairs in brsmatch() when too many are specified (#18)
Browse files Browse the repository at this point in the history
* Fix `brsmatch()` to reduce the number of pairs if too many for optimization
* Add testing to check in multiple scenarios
* Increment version number to 0.2.0.9002
* Update brsmatch documentation and NEWS.md
  • Loading branch information
skent259 authored Feb 19, 2024
1 parent e068b02 commit f17c71e
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 22 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: rsmatch
Title: Matching Methods for Time-Varying Observational Studies
Version: 0.2.0.9001
Version: 0.2.0.9002
Authors@R: c(
person("Sean", "Kent", , "skent259@gmail.com", role = c("aut", "cre", "cph"),
comment = c(ORCID = "0000-0001-8697-9069")),
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# rsmatch (development version)
* Update `brsmatch()` and `coxpsmatch()` to handle NA rows via removing them
* Fix `brsmatch()` to iteratively reduce the number of pairs if too many are specified

# rsmatch 0.2.0

Expand Down
86 changes: 68 additions & 18 deletions R/brsmatch.R
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
#'
#' Note that when using exact matching, the `n_pairs` are split roughly in
#' proportion to the number of treated subjects in each exact matching group.
#' This has a possibility of failing when `n_pairs` is large. If this happens
#' to you, we suggest manually performing exact matching, for example with
#' `split()`, and selecting `n_pairs` for each group interactively.
#' If you would like to control `n_pairs` exactly, we suggest manually
#' performing exact matching, for example with `split()`, and selecting
#' `n_pairs` for each group interactively.
#'
#' @param n_pairs The number of pairs desired from matching.
#' @param data A data.frame or similar containing columns matching the `id,
Expand Down Expand Up @@ -201,21 +201,11 @@ brsmatch <- function(
model <- .rsm_optimization_model(n_pairs, edges, bal, optimizer, verbose, balance)

.print_if(verbose, "Preparing to run optimization model")
if (optimizer == "gurobi") {
res <- gurobi::gurobi(model, params = list(OutputFlag = 1 * verbose))
matches <- res$x[grepl("f", model$varnames)]
} else if (optimizer == "glpk") {
res <- Rglpk::Rglpk_solve_LP(
model$obj,
model$mat,
model$dir,
model$rhs,
types = model$types,
max = model$max,
control = list(verbose = verbose, presolve = TRUE)
)
matches <- res$solution[grepl("f", model$varnames)]
}
res <- .solve_or_reduce_pairs(n_pairs, model, optimizer, verbose)
matches <- switch(optimizer,
"gurobi" = res$x[grepl("f", model$varnames)],
"glpk" = res$solution[grepl("f", model$varnames)]
)

matched_ids <- edges[matches == 1, c("trt_id", "all_id"), drop = FALSE]
return(matched_ids)
Expand Down Expand Up @@ -592,3 +582,63 @@ brsmatch <- function(
}
return(model)
}


#' Solve brsmatch model even if too many pairs specified
#'
#' If the `n_pairs` is too large, the model will be infeasible, and will return
#' a status code indicating this. This function will iteratively reduce the
#' number of pairs until the model becomes solvable, then will return the
#' solution with a warning.
#'
#' NOTE: gurobi functionality is untested, as I have since lost my license.
#' Code is based on the documentation at
#' https://www.gurobi.com/documentation/current/refman/r_grb.html
#'
#' @inheritParams brsmatch
#' @param model The model output from `.rsm_optimization_model()`
#'
#' @return The result from [Rglpk::Rglpk_solve_LP] after possible n_pair
#' reduction.
#'
#' @noRd
.solve_or_reduce_pairs <- function(n_pairs, model, optimizer, verbose) {
n_pairs_solve <- n_pairs

while (TRUE) {
if (optimizer == "gurobi") {
res <- gurobi::gurobi(model, params = list(OutputFlag = 1 * verbose))
} else if (optimizer == "glpk") {
res <- Rglpk::Rglpk_solve_LP(
model$obj,
model$mat,
model$dir,
model$rhs,
types = model$types,
max = model$max,
control = list(verbose = verbose, presolve = TRUE)
)
}
solved <- switch(optimizer,
"gurobi" = res$status == "OPTIMAL",
"glpk" = res$status == 0,
)
if (solved) {
break
}

n_pairs_solve <- n_pairs_solve - 1
# n pairs only appears in the first two model RHS constraints
model$rhs[1:2] <- c(n_pairs_solve, -n_pairs_solve)
}

if (n_pairs_solve != n_pairs) {
rlang::warn(
paste(
"Number of pairs reduced from", n_pairs, "to",
n_pairs_solve, "to create a solveable model."
)
)
}
return(res)
}
6 changes: 3 additions & 3 deletions man/brsmatch.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions tests/testthat/test-brsmatch.R
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,49 @@ test_that("brsmatch works when some input are NA", {
# can't hold everyone's hand all the time...
})


test_that("brsmatch works when too many pairs specified (#11)", {
brsmatch_on_oasis <- function(n_pairs, ...) {
brsmatch(
n_pairs, oasis,
id = "subject_id", time = "visit", trt_time = "time_of_ad",
...
)
}

check_for_glpk()

# With `balance = FALSE`
too_many <- brsmatch_on_oasis(n_pairs = 14, balance = FALSE) %>%
expect_warning("Number of pairs reduced")
enough <- brsmatch_on_oasis(n_pairs = 13, balance = FALSE)
expect_equal(too_many, enough)

# Way too many pairs
way_too_many <- brsmatch_on_oasis(n_pairs = 55, balance = FALSE) %>%
expect_warning("Number of pairs reduced")
expect_equal(way_too_many, too_many)

# With `balance = TRUE`
too_many <- brsmatch_on_oasis(n_pairs = 14, balance = TRUE) %>%
expect_warning("Number of pairs reduced")
enough <- brsmatch_on_oasis(n_pairs = 13, balance = TRUE)
expect_equal(too_many, enough)

# With `exact_match` variable
too_many <- brsmatch_on_oasis(n_pairs = 14, balance = FALSE, exact_match = c("m_f")) %>%
expect_warning("Number of pairs reduced")
enough <- brsmatch_on_oasis(n_pairs = 13, balance = FALSE, exact_match = c("m_f"))
expect_equal(too_many, enough)
# The warning message isn't ideal, but its okay for now


# With gurobi
check_for_gurobi()

too_many <- brsmatch_on_oasis(n_pairs = 14, balance = FALSE, options = list(optimizer = "gurobi")) %>%
expect_warning("Number of pairs reduced")
enough <- brsmatch_on_oasis(n_pairs = 13, balance = FALSE, options = list(optimizer = "gurobi"))
expect_equal(too_many, enough)

})

0 comments on commit f17c71e

Please sign in to comment.