The purpose of the rbqmR
package is to provide a repository of r-based
tools for the implementation of risk-based quality management.
Tools currently exist for
- Dynamic Quality Tolerance Limits (QTLs) using Bayesian Hierarchical Models (ongoing)
- Observed-Minus-Expected methodology
- Observed/Expected methodology (ongoing)
This package is a work-in-progress. It’s primary focus is dynamic QTLs. Other methodologies are included for completeness.
You can install the development version of rbqmR from GitHub with:
# install.packages("devtools")
devtools::install_github("openpharma/rbqmR")
We use example 2.7 of Berry et al (Berry SM 2011), described on pages 52 to 63, modifying the context so that rather than being a meta analysis of several different trials, we consider the data to represent the performance of different sites within a single trial. The exact metric being measured is immaterial, though it remains a summary of a binomial outcome.
data(berrySummary)
berrySummary %>% kable(digits = c(0, 0, 0, 2))
Site | Subjects | Events | ObservedResponse |
---|---|---|---|
1 | 20 | 20 | 1.00 |
2 | 10 | 4 | 0.40 |
3 | 16 | 11 | 0.69 |
4 | 19 | 10 | 0.53 |
5 | 14 | 5 | 0.36 |
6 | 46 | 36 | 0.78 |
7 | 10 | 9 | 0.90 |
8 | 9 | 7 | 0.78 |
9 | 6 | 4 | 0.67 |
The central tenet of the QTL methodology implemented in rbqmR
is not
that a current trial should behave in a similar fashion to a “similar”
historical or control trial, but rather that all sites within the
current trial should behave in a similar way. The justification for this
assumption is that the trial’s inclusion/exclusion criteria are designed
to minimise heterogeneity amongst the study population (save for that
induced by differences in treatment in comparative trials).
We fit the Bayesian Hierarchical Model described by Berry et al …
fitted <- berrySummary %>%
fitBayesBinomialModel(n = Subjects, r = Events)
#> Loading required namespace: rjags
#> INFO [2024-11-01 09:17:12] Status of model fitting: OK
fitted
#> $tab
#> # A tibble: 20,232 × 4
#> p a b q
#> <dbl> <dbl> <dbl> <int>
#> 1 0.491 9.73 2.87 15
#> 2 0.693 8.69 2.84 49
#> 3 0.508 5.32 2.22 16
#> 4 0.795 4.87 2.32 72
#> 5 0.923 3.27 2.23 94
#> 6 0.464 3.99 2.18 12
#> 7 0.613 3.06 2.48 33
#> 8 0.551 3.53 2.20 22
#> 9 0.635 4.92 2.42 37
#> 10 0.612 5.61 3.99 33
#> # ℹ 20,222 more rows
#>
#> $results
#>
#> JAGS model summary statistics from 20232 samples (chains = 2; adapt+burnin = 5000):
#>
#> Lower95 Median Upper95 Mean SD Mode MCerr MC%ofSD SSeff
#> p[10] 0.36447 0.69819 0.99969 0.68033 0.17298 -- 0.0013558 0.8 16279
#> a 2.1999 5.8971 9.9992 5.8967 2.2932 -- 0.060553 2.6 1434
#> b 0.57511 2.629 5.1368 2.7718 1.2458 -- 0.03174 2.5 1541
#>
#> AC.10 psrf
#> p[10] -0.0109 1.0001
#> a 0.2438 1.0016
#> b 0.23706 1.0018
#>
#> Total time taken: 1.9 seconds
#>
#>
#> $status
#> [1] "OK"
… and use the quantiles of the posterior distribution of the probability of an event to define the QTLs for this metric. This can be done in isolation (when the trial acts as its own control) or with reference to historical data obtained from similar previous studies.
When using a Bayesian Hierarchical Model, the probabilities associated with credible intervals are generally lower than those associated with similar frequentist models. This is because BHMs permit more sources of variation. Here, the BHM permits variation between the response rates at different sites, even when considering the overall event rate for the study. The corresponding frequentist analysis assumes that all sites share common event rate, thus assuming there is no inter-site variation.
The evaluatePointEstimateQTL
allows the comparison of an arbitrary
scalar summary statistic (which defaults to the mean) derived from the
estimate of the posterior distribution, with an arbitrary number of
lower and upper limits.
For example, the code below defines a QTL based on the mean of the
posterior distribution of the probability of an event. Call this
probability
berrySummary %>%
evaluatePointEstimateQTL(
posterior = fitted$tab,
metric = p,
observedMetric = ObservedResponse,
lower = c("warn" = 0.5, "action" = 0.4),
upper = c("warn" = 0.8, "action" = 0.9)
)
#> $status
#> [1] "OK"
#>
#> $qtl
#> [1] 0.6803784
#>
#> $data
#> # A tibble: 9 × 5
#> Site Subjects Events ObservedResponse Status
#> <int> <dbl> <dbl> <dbl> <chr>
#> 1 1 20 20 1 action
#> 2 2 10 4 0.4 warn
#> 3 3 16 11 0.688 OK
#> 4 4 19 10 0.526 OK
#> 5 5 14 5 0.357 warn
#> 6 6 46 36 0.783 OK
#> 7 7 10 9 0.9 warn
#> 8 8 9 7 0.778 OK
#> 9 9 6 4 0.667 OK
As with all evaluateXXXXQTL
functions, the return value of
evaluatePointEstimateQTL
is a list. The status
element indicates
whether or not the QTL’s limits have been breached. The qtl
element
gives the calculated value of the QTL metric and the data
element
returns a copy of the data.frame
containing the site level KRIs
augmented with a column indicating which, if any, of the various limits
were breached by that site.
Both the lower
and upper
parameters are optional (though at least
one must be given) and the number of limits, and their labels, are
arbitrary.
berrySummary %>%
evaluatePointEstimateQTL(
posterior = fitted$tab,
metric = p,
observedMetric = ObservedResponse,
upper = c("mild" = 0.6, "moderate" = 0.8, "severe" = 0.9)
)
If only one limit is defined, this can be provided as a scalar, in which
case it is labelled action
.
The function on which the QTL is based is specified by the stat
parameter of evaluatePointEstimateQTL
and can be a user-defined
function. For example, the following code fragments define QTLs based on
the median
berrySummary %>%
evaluatePointEstimateQTL(
posterior = fitted$tab,
metric = p,
stat = median,
observedMetric = ObservedResponse,
upper = c("warn" = 0.7, "action" = 0.9)
)
#> $status
#> [1] "OK"
#>
#> $qtl
#> [1] 0.6982975
#>
#> $data
#> # A tibble: 9 × 5
#> Site Subjects Events ObservedResponse Status
#> <int> <dbl> <dbl> <dbl> <chr>
#> 1 1 20 20 1 action
#> 2 2 10 4 0.4 OK
#> 3 3 16 11 0.688 OK
#> 4 4 19 10 0.526 OK
#> 5 5 14 5 0.357 OK
#> 6 6 46 36 0.783 warn
#> 7 7 10 9 0.9 warn
#> 8 8 9 7 0.778 warn
#> 9 9 6 4 0.667 OK
and 10th centile of the posterior distribution of
berrySummary %>%
evaluatePointEstimateQTL(
posterior = fitted$tab,
metric = p,
stat = function(x) quantile(x, probs = 0.1),
observedMetric = ObservedResponse,
upper = c("warn" = 0.3, "action" = 0.8)
)
#> $status
#> [1] "warn"
#>
#> $qtl
#> 10%
#> 0.4469523
#>
#> $data
#> # A tibble: 9 × 5
#> Site Subjects Events ObservedResponse Status
#> <int> <dbl> <dbl> <dbl> <chr>
#> 1 1 20 20 1 action
#> 2 2 10 4 0.4 warn
#> 3 3 16 11 0.688 warn
#> 4 4 19 10 0.526 warn
#> 5 5 14 5 0.357 warn
#> 6 6 46 36 0.783 warn
#> 7 7 10 9 0.9 action
#> 8 8 9 7 0.778 warn
#> 9 9 6 4 0.667 warn
Suppose previous experience tells us that the event probability in this type of study should be between 0.50 and 0.75. We define the QTL such that we require the posterior event probability for a new participant to be in the range 0.5 to 0.75 inclusive to be at least 60%, with a warning limit of 80%. Individual sites are flagged if their response rate is either below 40% or above 85%.
qtlProbInRange <- berrySummary %>%
evaluateProbabilityInRangeQTL(
posterior = fitted$tab,
metric = p,
observedMetric = ObservedResponse,
range = c(0.5, 0.75),
probs = c("warn" = 0.8, "action" = 0.6),
lower = 0.4,
upper = 0.85
)
qtlProbInRange
#> $status
#> [1] "action"
#>
#> $qtl
#> [1] 0.4652531
#>
#> $data
#> # A tibble: 9 × 5
#> Site Subjects Events ObservedResponse Status
#> <int> <dbl> <dbl> <dbl> <chr>
#> 1 1 20 20 1 action
#> 2 2 10 4 0.4 OK
#> 3 3 16 11 0.688 OK
#> 4 4 19 10 0.526 OK
#> 5 5 14 5 0.357 action
#> 6 6 46 36 0.783 OK
#> 7 7 10 9 0.9 action
#> 8 8 9 7 0.778 OK
#> 9 9 6 4 0.667 OK
Again, the QTL is breached, since the probability that the study-level event rate is in the range [0.5, 0.75] is only 0.47.
evaluatePointEstimateQTL
, evaluateProbabilityInRangeQTL
(and
evaluateSiteMetricQTL
discussed below) are wrappers around
evaluateCustomQTL
, which can be used to evaluate an arbitrary,
user-defined QTL rule. evaluateCustomQTL
takes the following
parameters:
data
: a tibble containing site-level observed metricsposterior
: a tibble containing the posterior distribution of the metric, usually obtained from a fit Bayes model function.f
: a function whose first two parameters aredata
andposterior
, in that ordered and with those namesstatusCol
: the column indata
that defines the status of the site. Here,Status
. Uses tidy evaluation....
: additional parameters passed tof
.
Essentially, all that evaluateCustomQTL
does is to perform some basic
checks on its parameter values and then return the value returned by
data %>% f(posterior, ...)
. So, for example, a simplified version of
evaluatePointEstimateQTL
that compares
berrySummary %>%
evaluateCustomQTL(
posterior = fitted$tab,
f = function(data, posterior) {
rv <- list()
rv$qtl <- posterior %>%
summarise(qtl = mean(p)) %>%
pull(qtl)
rv$status <- ifelse(rv$qtl < 0.6, "OK", "Breach")
rv$data <- data %>% mutate(Status = ifelse(ObservedResponse < 0.6, "OK", "Breach"))
rv
}
)
#> $qtl
#> [1] 0.6803784
#>
#> $status
#> [1] "Breach"
#>
#> $data
#> # A tibble: 9 × 5
#> Site Subjects Events ObservedResponse Status
#> <int> <dbl> <dbl> <dbl> <chr>
#> 1 1 20 20 1 Breach
#> 2 2 10 4 0.4 OK
#> 3 3 16 11 0.688 Breach
#> 4 4 19 10 0.526 OK
#> 5 5 14 5 0.357 OK
#> 6 6 46 36 0.783 Breach
#> 7 7 10 9 0.9 Breach
#> 8 8 9 7 0.778 Breach
#> 9 9 6 4 0.667 Breach
Especially early in development of a new compound, project teams often say they have no idea about what values of various metrics that could be used to define QTLs might be. For conventionally defined QTLs this can be a problem: an inappropriately chosen QTL might lead to a breach when, in reality, there is no issue. Basing the QTL on the centiles of the posterior distribution of the metric obtained from the QTL model just fitted, and recalling that the purpose of inclusion/exclusion criteria are to minimise heterogeneity, we can avoid this problem.
The evaluateSiteMetricQTL
function calculates thresholds (for example
warning and action limits) by translating quantiles of the posterior
distribution of the metric to the real-life scale on which the metric is
measured. Again, we use the Berry data as an example.
rvSiteMetrics <- berrySummary %>%
evaluateSiteMetricQTL(
posterior = fitted$tab,
metric = p,
observedMetric = ObservedResponse,
lower = c("action" = 0.05, "warn" = 0.2),
upper = c("action" = 0.95, "warn" = 0.8)
)
The quantiles
element of the return value contains the mappings from
quantile of the posterior to observed values of the metric. For example,
the first row of rvSiteMetrics$quantiles
shows that the lower action
limit is the 5th centile of the posterior, which corresponds to an event
probability of 0.365.
rvSiteMetrics$quantiles
#> # A tibble: 4 × 4
#> Threshold Status Quantile p
#> <chr> <chr> <dbl> <dbl>
#> 1 Lower action 0.05 0.365
#> 2 Lower warn 0.2 0.538
#> 3 Upper warn 0.8 0.834
#> 4 Upper action 0.95 0.932
As before, the data
element of the list contains a copy of the site
metric dataset, augmented by a column (named Status
by default) that
allocates the observed site-level metrics (KRIs) to a band defined by
the thresholds in lower
and upper
.
rvSiteMetrics$data
#> # A tibble: 9 × 5
#> Site Subjects Events ObservedResponse Status
#> <int> <dbl> <dbl> <dbl> <chr>
#> 1 1 20 20 1 warn
#> 2 2 10 4 0.4 warn
#> 3 3 16 11 0.688 OK
#> 4 4 19 10 0.526 warn
#> 5 5 14 5 0.357 action
#> 6 6 46 36 0.783 OK
#> 7 7 10 9 0.9 warn
#> 8 8 9 7 0.778 OK
#> 9 9 6 4 0.667 OK
NB
rvSiteMetrics$data
contains incorrect values in theStatus
column. See issue #12. Theqtl
element of the return value contains counts of sites by threshold name.
rvSiteMetrics$qtl
#> # A tibble: 3 × 2
#> Status N
#> <chr> <int>
#> 1 OK 4
#> 2 action 1
#> 3 warn 4
Finally, the status
of the returned QTL is "OK"
by default.
rvSiteMetrics$status
#> [1] "OK"
However, evaluateSiteMetricQTL
can be passed a function that can apply
an arbitrary rule to determine whether or not a breach has occurred. In
the example below, the function’s only argument is the qtl
element of
evaluateSiteMetricQTL
’s return value. For example, given that we have
nine sites in our fictitious example and we have calculated a 90%
posterior credible interval, it’s reasonable to expect one site to lie
outside this range. So we might say that a QTL breach has occurred if
two or more sites lie outside the credible interval:
(berrySummary %>%
evaluateSiteMetricQTL(
posterior = fitted$tab,
metric = p,
observedMetric = ObservedResponse,
lower = c("action" = 0.5, "warn" = 0.6),
upper = c("action" = 0.9, "warn" = 0.8),
statusFunc = function(d) {
ifelse(
d %>%
dplyr::filter(Status == "action") %>%
dplyr::pull(N) >= 2, "action", "OK"
)
}
))$status
#> [1] "action"
Take, as an example, a QTL that requires a study level metric to lie within a given range, as illustrated above.
fitted$tab %>%
createQtlPlot(
targetRange = list("lower" = 0.5, "upper" = 0.75),
observedMetric = fitted$tab %>% summarise(Mean = mean(p)) %>% pull(Mean)
)
The site-level KRIs can be added to the plot to help focus attention where intervention is likely to have the largest effect.
fitted$tab %>%
createQtlPlot(
targetRange = list("lower" = 0.5, "upper" = 0.75),
observedMetric = fitted$tab %>% summarise(Mean = mean(p)) %>% pull(Mean),
actionLimits = list(
list(
"lower" = rvSiteMetrics$quantiles %>% filter(Threshold == "Upper", Status == "action") %>% pull(p),
"upper" = NA,
"alpha" = 0.3,
"colour" = "goldenrod1"
),
list(
"lower" = NA,
"upper" = rvSiteMetrics$quantiles %>% filter(Threshold == "Lower", Status == "action") %>% pull(p),
"alpha" = 0.3,
"colour" = "goldenrod1"
)
),
siteData = berrySummary,
siteSize = Subjects,
siteMetric = ObservedResponse
)
The KRIs associated with a QTL can be presented graphically. rmqmR
includes the createQtlBubblePlot
function, which presents the KRIs
(possibly from multiple evaluations of a QTL) in the form of a bubble
plot overlaid with a box-and-whisker plot. Site-level data may be
grouped in an arbitrary fashion, or not at all. Optionally, reference
lines that correspond to QTL thresholds may be added.
berrySummary %>%
add_column(Snapshot = "End of study") %>%
add_column(Location = c(rep("EU", 4), rep("US", 5))) %>%
createQtlBubblePlot(
x = Snapshot,
y = ObservedResponse,
size = Subjects,
group = Location,
boxWidth = 0.1,
limits = list(
list(
label = "QTL (85%)",
colour = "red",
type = "dashed",
y = 0.85,
x = 1.25,
vjust = -1
),
list(
label = "Sec Lim (75%)",
colour = "goldenrod",
type = "dotted",
y = 0.75,
x = 1.25,
vjust = 1.25
)
)
)
#> Warning: The following aesthetics were dropped during statistical transformation: size.
#> ℹ This can happen when ggplot fails to infer the correct grouping structure in
#> the data.
#> ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
#> variable into a factor?
We generate some random data similar to that used by (Gilbert 2020), after setting a seed for reproducibility.
In order to illustrate what happens when a QTL is breached, we set the probability that a participant reports an event to 0.13, even though the QTL process will assume the event rate is 0.10…
set.seed(011327)
randomData <- tibble(
Subject = 1:400,
Event = rbinom(400, 1, 0.13)
)
… and create an observed-expected table …
omeTable <- randomData %>%
createObservedMinusExpectedTable(
timeVar = Subject,
eventVar = Event,
eventArray = 1,
expectedRate = 0.1,
maxTrialSize = 400
)
… and plot the corresponding graph.
omeTable %>%
createObservedMinusExpectedPlot()
We can see that the trial breached a warning limit. When did this first happen?
omeTable %>%
filter(Status != "OK") %>%
head(1) %>%
select(-contains("Action"), -SubjectIndex) %>%
kable(
col.names = c("Subject", "Event", "Cumulative Events", "O - E", "Status", "Lower", "Upper"),
caption = "First breach of an action or warning limit"
) %>%
add_header_above(c(" " = 5, "Warning Limits" = 2))
Warning Limits |
||||||
---|---|---|---|---|---|---|
Subject | Event | Cumulative Events | O - E | Status | Lower | Upper |
62 | 1 | 13 | 6.8 | WARN | -5.2 | 5.8 |
Katz et al (Katz D 1978) calculate the confidence interval for the ratio of two binomial random variables. We use this to determine QTLs for the ratio of observed over expected proportions. The variability associated with a ratio suggests that this methodology is likely to be useful only for large studies with low expected event rates.
We require historical trial data to implement this methodology.
Suppose we have data on 10,000 historical patients who have reported a given event at a rate of 1.4%. We are planning a 1500 patient trial and have no reason to suppose the event rate in the trial will be any different from what has been seen in the past.
createObservedOverExpectedTable(
nHistorical = 10000,
historicalRate = 0.014,
expectedRate = 0.014,
nObservedRange = seq(50, 1500, 25)
) %>%
createObservedOverExpectedPlot()
As the trial is executed, the observed data can be added to the table and the plot.
observedData <- tibble(
NObserved = c(250, 500, 750, 1000),
ObservedRate = 100 * c(2, 9, 15, 16) / NObserved
)
createObservedOverExpectedTable(
nHistorical = 10000,
historicalRate = 0.014,
expectedRate = 0.014,
nObservedRange = seq(50, 1500, 25),
observedData = observedData,
observedRate = ObservedRate
) %>% createObservedOverExpectedPlot(observedRate = ObservedRate)
TODO Need to check interpretation of parameters
At the time of writing (late 2022) The TransCelerate Quality Tolerance Limit Framework (Transcelerate 2020) lists metrics that are exclusively binary in nature. There are many other potential metrics that are non-binary and which may provide insight into the conduct of the trial. For example,
- The number of episodes of rescue medication (as opposed to the percentage or number of trial participants on rescue medication)
- Time to withdrawal of consent (as opposed to the percentage or number of trial participants with withdrawal of informed consent)
As well as other metrics that can’t easily be dichotomised
- Drug plasma levels
- Number of (S)AEs reported per time unit of drug exposure
- Time to respond to data queries
The Bayesian QTL framework implemented in rbqmR
can easily be extended
to include these other data types.
We use data on the numbers of Prussian cavalry officers kicked to death by horses (Bortkiewicz 1898) to illustrate the method.
data("cavalryDeaths")
cavalryDeaths
#> # A tibble: 280 × 3
#> Year Corps Deaths
#> <int> <fct> <int>
#> 1 1875 Guards 0
#> 2 1875 Corps 1 0
#> 3 1875 Corps 2 0
#> 4 1875 Corps 3 0
#> 5 1875 Corps 4 0
#> 6 1875 Corps 5 0
#> 7 1875 Corps 6 0
#> 8 1875 Corps 7 1
#> 9 1875 Corps 8 1
#> 10 1875 Corps 9 0
#> # ℹ 270 more rows
Regard different cavalry Corps as “sites” and regard the number of years for which data were collected as “exposure”.
cavalrySummary <- cavalryDeaths %>%
group_by(Corps) %>%
summarise(
Deaths = sum(Deaths),
TotalTime = n(),
.groups = "drop"
) %>%
mutate(DeathRate = Deaths / TotalTime)
cavalrySummary
#> # A tibble: 14 × 4
#> Corps Deaths TotalTime DeathRate
#> <fct> <int> <int> <dbl>
#> 1 Guards 16 20 0.8
#> 2 Corps 1 16 20 0.8
#> 3 Corps 2 12 20 0.6
#> 4 Corps 3 12 20 0.6
#> 5 Corps 4 8 20 0.4
#> 6 Corps 5 11 20 0.55
#> 7 Corps 6 17 20 0.85
#> 8 Corps 7 12 20 0.6
#> 9 Corps 8 7 20 0.35
#> 10 Corps 9 13 20 0.65
#> 11 Corps 10 15 20 0.75
#> 12 Corps 11 25 20 1.25
#> 13 Corps 14 24 20 1.2
#> 14 Corps 15 8 20 0.4
Although not necessary here, because data for all Corps was recorded for
the same amount of time, the Poisson model used by rbqmR
adjusts risk
by total exposure at the site.
getModelString("poisson")
#> model {
#> for (i in 1:k) {
#>
#> events[i] ~ dpois(mu[i])
#> mu[i] <- lambda[i]*exposure[i]
#>
#> lambda[i] ~ dgamma(shape, 1/scale)
#> }
#> scale ~ dgamma(1, 1)
#> shape ~ dgamma(1, 1)
#> }
Fitting the model is straightforward.
poissonFit <- cavalrySummary %>%
fitBayesPoissonModel(Deaths, TotalTime)
#> INFO [2024-11-01 09:17:16] Status of model fitting: OK
poissonFit$tab %>%
createQtlPlot(
metric = lambda,
siteData = cavalrySummary,
siteSize = TotalTime,
siteMetric = DeathRate
) +
labs(x = "Deaths per year")
sessionInfo()
#> R version 4.4.1 (2024-06-14)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 22.04.5 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.20.so; LAPACK version 3.10.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] rbqmR_0.0.0.9002 testthat_3.2.1.1 lubridate_1.9.3 forcats_1.0.0
#> [5] stringr_1.5.1 dplyr_1.1.4 purrr_1.0.2 readr_2.1.5
#> [9] tidyr_1.3.1 tibble_3.2.1 ggplot2_3.5.1 tidyverse_2.0.0
#> [13] kableExtra_1.4.0 magrittr_2.0.3
#>
#> loaded via a namespace (and not attached):
#> [1] toOrdinal_1.3-0.0 gtable_0.3.5 xfun_0.48
#> [4] htmlwidgets_1.6.4 devtools_2.4.5 remotes_2.5.0
#> [7] lattice_0.22-6 tzdb_0.4.0 vctrs_0.6.5
#> [10] tools_4.4.1 generics_0.1.3 parallel_4.4.1
#> [13] fansi_1.0.6 highr_0.11 pkgconfig_2.0.3
#> [16] desc_1.4.3 lifecycle_1.0.4 farver_2.1.2
#> [19] compiler_4.4.1 brio_1.1.5 munsell_0.5.1
#> [22] runjags_2.2.2-4 httpuv_1.6.15 htmltools_0.5.8.1
#> [25] usethis_3.0.0 yaml_2.3.10 crayon_1.5.3
#> [28] pillar_1.9.0 later_1.3.2 urlchecker_1.0.1
#> [31] ellipsis_0.3.2 cachem_1.1.0 sessioninfo_1.2.2
#> [34] mime_0.12 tidyselect_1.2.1 digest_0.6.37
#> [37] stringi_1.8.4 labeling_0.4.3 rprojroot_2.0.4
#> [40] fastmap_1.2.0 grid_4.4.1 colorspace_2.1-1
#> [43] cli_3.6.3 pkgbuild_1.4.4 utf8_1.2.4
#> [46] withr_3.0.1 scales_1.3.0 promises_1.3.0
#> [49] timechange_0.3.0 lambda.r_1.2.4 rmarkdown_2.28
#> [52] futile.logger_1.4.3 rjags_4-16 hms_1.1.3
#> [55] coda_0.19-4.1 memoise_2.0.1 shiny_1.9.1
#> [58] evaluate_1.0.1 knitr_1.48 miniUI_0.1.1.1
#> [61] viridisLite_0.4.2 futile.options_1.0.1 profvis_0.4.0
#> [64] rlang_1.1.4 Rcpp_1.0.13 xtable_1.8-4
#> [67] glue_1.8.0 formatR_1.14 xml2_1.3.6
#> [70] pkgload_1.4.0 svglite_2.1.3 rstudioapi_0.16.0
#> [73] R6_2.5.1 systemfonts_1.1.0 fs_1.6.4
Berry SM, Lee JJ, Carlin BP. 2011. Bayesian Adaptive Methods for Clinical Trials. CRC Press.
Bortkiewicz, Ladislaus von. 1898. “Das Gesetz Der Kleinen Zahlen.”
Gilbert, SA. 2020. “Implementing Quality Tolerance Limits at a Large Pharmaceutical Company.” PharmaSUG.
Katz D, Azen SP, Baptista J. 1978. “Obtaining Confidence Intervals for the Risk Ratio in Cohort Studies.” Biometrics 34 (3): 469–74.
Transcelerate. 2020. “Quality Tolerance Limits Framework.” Research report. Transcelerate Biopharma Inc. 2020. https://www.transceleratebiopharmainc.com/wp-content/uploads/2020/10/TransCelerate_InterpretationsOfClinical-GuidancesAndRegulations_Quality-Tolerance-Limits-Framework_October-2020.pdf.