Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
740d8cd
ignore .vscode folder, don't format anything with air for now
danielinteractive Nov 18, 2025
1cb3052
add linter config
danielinteractive Nov 18, 2025
65a3676
ignores update, allow mixed lower.tail in decision1S
danielinteractive Nov 18, 2025
e50a012
add convenience methods for decision1S objects
danielinteractive Nov 18, 2025
04cf33e
first prototype for normal outcome with 1 sample function
danielinteractive Nov 18, 2025
45bc029
remove missing(theta), was already deprecated
danielinteractive Nov 26, 2025
3edbdea
add pos1S normal just to confirm that it works with mixed lower.tail
danielinteractive Nov 26, 2025
0a3fd9a
run oc2s tests successfully
danielinteractive Nov 26, 2025
c6c0e78
clean oc2S normal a bit
danielinteractive Nov 26, 2025
e54313a
fix decision1S
danielinteractive Nov 26, 2025
fece330
add methods for decision2S
danielinteractive Nov 26, 2025
a78941d
decision2S_boundary also returns two functions now if mixed lower.tai…
danielinteractive Nov 26, 2025
61f3948
first version of normal oc2S with mixed boundaries, seems to be working?
danielinteractive Nov 26, 2025
66dbdda
improve test
danielinteractive Nov 26, 2025
63a468f
adapt pos2S.normMix
danielinteractive Jan 12, 2026
55ba904
tested decision2S
danielinteractive Jan 12, 2026
5b87a4c
tested decision2S_boundary
danielinteractive Jan 12, 2026
8bb70ef
tested pos2S
danielinteractive Jan 12, 2026
86b80d7
add snapshot
danielinteractive Jan 12, 2026
66da991
add Daniel as ctb
danielinteractive Jan 12, 2026
225a604
air formatting
danielinteractive Jan 19, 2026
711b59d
Merge branch 'air_formatting'
danielinteractive Jan 19, 2026
51b3875
new structure for decision1S, incl. new class decision1S_2sided
danielinteractive Jan 19, 2026
a81b407
adapt decision1S_boundary.normMix
danielinteractive Jan 19, 2026
995d7b8
slightly adapt oc1S and pos1S
danielinteractive Jan 19, 2026
d59b8ef
adapt decision2S, too
danielinteractive Jan 19, 2026
74dc200
adapt tests
danielinteractive Jan 19, 2026
028dc93
adapt decision2S_boundary
danielinteractive Jan 19, 2026
21b4112
adapt oc2S
danielinteractive Jan 19, 2026
4bb97ee
adapt pos2S
danielinteractive Jan 19, 2026
791e46d
add NEWS entry
danielinteractive Jan 19, 2026
f0329a7
update snapshots
danielinteractive Jan 19, 2026
0476813
address review comments
danielinteractive Jan 26, 2026
721b28e
add decision1S 2-sided example
danielinteractive Jan 26, 2026
096829d
add example for decision2S
danielinteractive Jan 26, 2026
d107581
add example for oc1S
danielinteractive Jan 26, 2026
7593ee1
update docs
danielinteractive Jan 26, 2026
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
3 changes: 3 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ src/stan_files/.*\.hpp$
^Makefile$
^LICENSE$
^vignettes/articles$
^\.vscode$
^[.]?air[.]toml$
^\.lintr$
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
.RData
.Ruserdata
docs
src/*.so
src/*.o
src/stan_files/*.hpp
src/stan_files/*.o
src/
*.*~
inst/sbc/sbc*html
.vscode/
*.dll
.Renviron
5 changes: 5 additions & 0 deletions .lintr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
linters: linters_with_defaults(
line_length_linter(120),
object_name_linter = NULL,
commented_code_linter = NULL
)
4 changes: 4 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ S3method(print,EMnmm)
S3method(print,betaBinomialMix)
S3method(print,betaMix)
S3method(print,decision1S)
S3method(print,decision1S_2sided)
S3method(print,decision2S)
S3method(print,decision2S_2sided)
S3method(print,dlink)
S3method(print,gMAP)
S3method(print,gMAPpred)
Expand Down Expand Up @@ -135,6 +137,7 @@ export(gMAP)
export(inv_logit)
export(likelihood)
export(logit)
export(lower)
export(mixbeta)
export(mixcombine)
export(mixfit)
Expand Down Expand Up @@ -165,6 +168,7 @@ export(rmix)
export(rmixdiff)
export(robustify)
export(sigma)
export(upper)
export(write_mix_json)
import(Formula)
import(Rcpp)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Enhancements

* Reformat R sources using `Air`.
* Extend normal outcome functions to two-sided decisions: `decision1S` and `decision2S` now allow `lower.tail` to have as many elements as `pc` to allow the specification of two-sided decision boundaries (mixed `lower.tail` elements, i.e. some specifying "lower" and some "upper") to capture intermediate result scenarios. This is currently only respected by the normal outcome functions (methods for `normMix`), because it is work in progress.

# RBesT 1.8-2 - April 25th, 2025

Expand Down
133 changes: 122 additions & 11 deletions R/decision1S.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
#' @param pc Vector of critical cumulative probabilities.
#' @param qc Vector of respective critical values. Must match the length of `pc`.
#' @param lower.tail Logical; if `TRUE` (default), probabilities
#' are \eqn{P(X \leq x)}, otherwise, \eqn{P(X > x)}.
#' are \eqn{P(X \leq x)}, otherwise, \eqn{P(X > x)}. Either length 1 or same
#' length as `pc`.
#' @param x Two-sided decision function.
#'
#' @details The function creates a one-sided decision function which
#' @details For `lower.tail` being either `TRUE` or `FALSE`,
#' the function creates a one-sided decision function which
#' takes two arguments. The first argument is expected to be a mixture
#' (posterior) distribution. This distribution is tested whether it
#' fulfills all the required threshold conditions specified with the
Expand All @@ -25,6 +28,10 @@
#'
#' \deqn{\Pi_i H_i(P(\theta \leq q_{c,i}) - p_{c,i} ).}
#'
#' For the case of a boolen vector given to `lower.tail` the
#' direction of each decision aligns respectively, and a two-sided
#' decision function is created.
#'
#' When the second argument is set to `TRUE` a distance metric is
#' returned component-wise per defined condition as
#'
Expand All @@ -36,7 +43,9 @@
#'
#' @family design1S
#'
#' @return The function returns a decision function which takes two
#' @return The function returns a decision function (of class
#' `decision1S` for one-sided, and of class `decision1S_2sided`
#' for two-sided decisions) which takes two
#' arguments. The first argument is expected to be a mixture
#' (posterior) distribution which is tested if the specified
#' conditions are met. The logical second argument determines if the
Expand All @@ -45,6 +54,11 @@
#' log-space, i.e. the distance is 0 at the decision boundary,
#' negative for a 0 decision and positive for a 1 decision.
#'
#' For two-sided decision functions, the two components can be
#' extracted with functions [lower()] and [upper()]. The distance
#' as calculated by the decision function is returned as a list with
#' components `lower` and `upper`.
#'
#' @references Neuenschwander B, Rouyrre N, Hollaender H, Zuber E,
#' Branson M. A proof of concept phase II non-inferiority
#' criterion. *Stat. in Med.*. 2011, 30:1618-1627
Expand All @@ -53,7 +67,7 @@
#'
#' # see Neuenschwander et al., 2011
#'
#' # example is for a time-to-event trial evaluating non-inferiority
#' # example is for a time-to-event trial evaluating non-inferiority (NI)
#' # using a normal approximation for the log-hazard ratio
#'
#' # reference scale
Expand Down Expand Up @@ -99,9 +113,52 @@
#' # here with HR of 0.8 for 40 events
#' decComb(postmix(flat_prior, m = log(0.8), n = 40))
#'
#' # A two-sided decision function can be useful to determine if
#' # certain intermediate (i.e. neither "go" nor "stop") decisions
#' # are to be made based on the posterior distribution.
#' # For example, in the above situation we might have an intermediate
#' # scenario where the trial is significant for non-inferiority but
#' # the mean estimate is in an intermediate range, say between theta_c
#' # theta_f:
#' theta_f <- 0.3
#' decCombIntermediate <- decision1S(
#' c(1 - alpha, 0.5, 0.8),
#' c(theta_ni, theta_c, theta_f),
#' lower.tail = c(TRUE, FALSE, TRUE)
#' )
#' # Not fulfilled for the prior:
#' decCombIntermediate(flat_prior)
#' # But for a hypothetical trial outcome with HR 1.2 and 300 events:
#' decCombIntermediate(postmix(flat_prior, m = log(1.2), n = 300))
#'
#' @export
decision1S <- function(pc = 0.975, qc = 0, lower.tail = TRUE) {
assert_that(length(pc) == length(qc))
assert_numeric(pc)
assert_numeric(qc, len = length(pc))
assert_logical(lower.tail)
assert_true(length(lower.tail) == 1L || length(lower.tail) == length(pc))
lower.tail <- scalar_if_same(lower.tail)

is_two_sided <- length(lower.tail) > 1

if (is_two_sided) {
return(create_decision1S_2sided(pc, qc, lower.tail))
} else {
return(create_decision1S_1sided(pc, qc, lower.tail))
}
}

#' @keywords internal
scalar_if_same <- function(x) {
if (length(x) > 1 && all(x == x[1])) {
return(x[1])
}
x
}

#' Internal Constructor for 1 Sample One-sided Decision Function
#' @keywords internal
create_decision1S_1sided <- function(pc, qc, lower.tail) {
lpc <- log(pc)
fun <- function(mix, dist = FALSE) {
test <- pmix(mix, qc, lower.tail = lower.tail, log.p = TRUE) - lpc
Expand All @@ -113,21 +170,75 @@ decision1S <- function(pc = 0.975, qc = 0, lower.tail = TRUE) {
attr(fun, "pc") <- pc
attr(fun, "qc") <- qc
attr(fun, "lower.tail") <- lower.tail

Copy link
Collaborator

Choose a reason for hiding this comment

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

since there is no lower/upper attribute, one would get null when requesting these. Wouldn't it make more sense to have lower/upper attribute being defined for whatever the 1S is for... the other one should be set to NULL, which probably does not need defintion.

Copy link
Author

Choose a reason for hiding this comment

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

I understand, that would also be possible. So far I was trying to be fully backwards compatible with this enhancement, i.e. I did not want to change the current one-sided objects or structure. Therefore I did not try to add attributes here or change the overall class structure for one-sided. If you think that is not important, then I can try to make the one-sided and two-sided more consistent with each other (i.e. closer to the more general two-sided)

class(fun) <- c("decision1S", "function")
fun
}

#' Internal Constructor for 1 Sample Two-sided Decision Function
#' @keywords internal
create_decision1S_2sided <- function(pc, qc, lower.tail) {
use_lower <- which(lower.tail)
use_upper <- which(!lower.tail)
assert_true(length(use_lower) > 0 && length(use_upper) > 0)

lower_part <- create_decision1S_1sided(pc[use_lower], qc[use_lower], TRUE)
upper_part <- create_decision1S_1sided(pc[use_upper], qc[use_upper], FALSE)

fun <- function(mix, dist = FALSE) {
dl <- lower_part(mix, dist)
du <- upper_part(mix, dist)
if (dist) {
return(list(lower = dl, upper = du))
}
as.numeric(all(dl > 0) && all(du > 0))
}
attr(fun, "lower") <- lower_part
attr(fun, "upper") <- upper_part

class(fun) <- c("decision1S_2sided", "function")
fun
}

#' @rdname decision1S
#' @export
print.decision1S <- function(x, ...) {
cat("1 sample decision function\n")
cat("Conditions for acceptance:\n")
lower <- function(x) {
assert_multi_class(x, c("decision1S_2sided", "decision2S_2sided"))
attr(x, "lower")
}

#' @rdname decision1S
#' @export
upper <- function(x) {
assert_multi_class(x, c("decision1S_2sided", "decision2S_2sided"))
attr(x, "upper")
}

#' @keywords internal
print_decision1S_1sided <- function(x) {
qc <- attr(x, "qc")
pc <- attr(x, "pc")
low <- attr(x, "lower.tail")
cmp <- ifelse(low, "<=", ">")
for (i in seq_along(qc)) {
cat(paste0("P(theta ", cmp, " ", qc[i], ") > ", pc[i], "\n"))
}
cat(paste0("P(theta ", cmp, " ", qc, ") > ", pc, "\n"), sep = "")
}

#' @export
print.decision1S <- function(x, ...) {
cat("1 sample decision function\n")
cat("Conditions for acceptance:\n")
print_decision1S_1sided(x)
invisible(x)
}

#' @export
print.decision1S_2sided <- function(x, ...) {
cat("1 sample decision function (two-sided)\n")
cat("Conditions for acceptance:\n")
cat("Lower tail conditions:\n")
print_decision1S_1sided(lower(x))
cat("Upper tail conditions:\n")
print_decision1S_1sided(upper(x))
invisible(x)
}

Expand Down
69 changes: 61 additions & 8 deletions R/decision1S_boundary.R
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
#' aligned with the cumulative density function definition within R
#' (see for example [pbinom()]).
#'
#' @return Returns the critical value \eqn{y_c}.
#' @return Returns the critical value \eqn{y_c}. For two-sided
#' decision functions a named vector with components
#' `lower_or_equal_than` and `higher_than` is returned, containing
#' the critical values for the lower and upper decision boundaries.
#'
#' @family design1S
#'
Expand Down Expand Up @@ -157,13 +160,67 @@ decision1S_boundary.normMix <- function(
eps = 1e-6,
...
) {
## distributions of the means of the data generating distributions
## for now we assume that the underlying standard deviation
## matches the respective reference scales
# Get the default sigma if not provided already here to only message once.
if (missing(sigma)) {
sigma <- RBesT::sigma(prior)
message("Using default prior reference scale ", sigma)
}

if (is(decision, "decision1S_2sided")) {
decision1S_boundary_normMix_2sided(
prior,
n,
decision,
sigma,
eps
)
} else {
decision1S_boundary_normMix_1sided(
prior,
n,
decision,
sigma,
eps
)
}
}

#' @keywords internal
decision1S_boundary_normMix_2sided <- function(
prior,
n,
decision,
sigma,
eps
) {
crit_lower <- decision1S_boundary_normMix_1sided(
prior,
n,
lower(decision),
sigma,
eps
)
crit_upper <- decision1S_boundary_normMix_1sided(
prior,
n,
upper(decision),
sigma,
eps
)
c(lower_or_equal_than = crit_lower, higher_than = crit_upper)
}

#' @keywords internal
decision1S_boundary_normMix_1sided <- function(
prior,
n,
decision,
sigma,
eps
) {
## distributions of the means of the data generating distributions
## for now we assume that the underlying standard deviation
## matches the respective reference scales
assert_number(sigma, lower = 0)

sd_samp <- sigma / sqrt(n)
Expand All @@ -172,9 +229,6 @@ decision1S_boundary.normMix <- function(

## change the reference scale of the prior such that the prior
## represents the distribution of the respective means
## mean_prior <- prior
## sigma(mean_prior) <- sd_samp

m <- summary(prior, probs = c())["mean"]

lim <- qnorm(p = c(eps / 2, 1 - eps / 2), mean = m, sd = sd_samp)
Expand All @@ -185,7 +239,6 @@ decision1S_boundary.normMix <- function(
crit
}


#' @templateVar fun decision1S_boundary
#' @template design1S-poisson
#' @export
Expand Down
Loading
Loading