Direct Simulation-Based FPR Calibration at a Specific Threshold Pair
Source:R/fpr_calibration.R
fpr_calibration.RdEstimates the procedure-level false-positive rate (FPR) for a
forestsearch analysis at a specific \((c_1, c_2)\)
threshold pair by running nsims null simulations. No power-law
model or threshold-independence assumption is required.
Usage
fpr_calibration(
c1,
c2,
df.analysis = NULL,
fs_params = list(),
nsims = 200L,
nsims_verify = 0L,
null_fn = NULL,
n_sample = NULL,
n_min = 60L,
outcome_type_approx = NULL,
d_eff_approx = NULL,
prop_cens = 0,
p_event = 0.5,
sigma_y = 1,
sim_seed = 42L,
n_workers = 1L,
quiet = FALSE
)Arguments
- c1
Numeric scalar. The screening threshold on a candidate subgroup's own effect.
c1is this function's name forforestsearch'seffect.threshold(legacyhr.threshold), and is passed through to it under that name, on the natural scale –forestsearch()resolves the comparison scale per estimand.- c2
Numeric scalar. The per-split effect threshold.
c2is this function's name forforestsearch'sconsistency.threshold(legacyhr.consistency), passed through under that name on the natural scale. It is an effect threshold applied within each split half, not the consistency rate: the rate ispconsistency.thresholdand travels infs_params. Must satisfyc2 <= c1.- df.analysis
Data frame. The analyst's dataset. Used directly in permutation mode; also used to extract N for the per-subgroup approximation. Can be
NULLifnull_fnis supplied andn_sampleis given explicitly.- fs_params
Named list. All forestsearch() arguments except
hr.thresholdandhr.consistency(which are set toc1/c2) anddf.analysis(supplied separately). Must include at minimumoutcome.name,treat.name,id.name, and — for binary/continuous/count —outcome_typeandeffect_measure.- nsims
Integer. Number of null replicates. Default 200. At FPR ~ 10%,
nsims = 200gives a 95% CI half-width of ~±2.1 pp;nsims = 500gives ~±1.3 pp.- nsims_verify
Integer. Number of independent verification replicates. Uses a separate seed range from the calibration batch to provide an honest check of the corrected FPR. Default 0 (skip verification). Set to
nsimsor larger for production use.- null_fn
Function or
NULL. If provided, called asnull_fn(seed)and must return a data frame with the same column structure asdf.analysis. Overrides permutation mode. DefaultNULL(permutation mode).- n_sample
Integer or
NULL. Sample size used whennull_fnis provided. Ignored in permutation mode (N is taken fromnrow(df.analysis)).- n_min
Integer. Minimum subgroup size for the per-subgroup approximation \(P_1\). Should match
fs_params$n.min. Default 60.- outcome_type_approx
Character. One of
"binary","survival","count","continuous". Used only for computing the asymptotic \(P_1\) approximation. IfNULL(default), inferred fromfs_params$outcome_type.- d_eff_approx
Numeric or
NULL. Effective information for \(P_1\). IfNULL(default), estimated fromdf.analysis(orn_sample) usingd_eff_*(). Supply explicitly if you want to override (e.g. use an expected event count rather than observed).- prop_cens
Numeric. Censoring proportion for survival outcomes. Used only when
d_eff_approxis not supplied andoutcome_type_approx = "survival". Default 0.- p_event
Numeric. Event probability for binary outcomes. Used only when
d_eff_approxis not supplied andoutcome_type_approx = "binary". Default 0.5.- sigma_y
Numeric. Residual SD for continuous outcomes. Default 1.
- sim_seed
Integer. Base random seed. Each replicate uses
sim_seed + i. Default 42.- n_workers
Integer. Parallel workers for the foreach loop. Default 1 (sequential). Set to
parallel::detectCores() - 1for production runs.- quiet
Logical. Suppress progress messages. Default
FALSE.
Value
An object of class "fpr_calibration" with components:
fprNumeric. Estimated procedure-level FPR.
fpr_ciNumeric vector of length 2. 95% Wilson CI.
n_foundInteger. Number of replicates where a subgroup was found.
nsimsInteger. Total replicates attempted.
n_failedInteger. Replicates that errored.
P1Numeric. Per-subgroup approximation \(P_1\) at
(c1, c2).d_effNumeric. Effective information used for \(P_1\).
L_eff_impliedNumeric. Implied \(L_{\text{eff}}\) from the simulation result: \(\log(1 - \text{FPR}) / \log(1 - P_1)\).
c1, c2Numeric. The threshold pair.
NInteger. Sample size.
modeCharacter.
"permutation"or"dgm".call_argsList. Key call arguments for reproducibility.
Details
Two null-generation modes are supported:
null_fnprovidedEach replicate draws a fresh null dataset from
null_fn(seed). Use this when you have a parametric data generating mechanism (DGM) or want to mimic a specific study design.df.analysisonly (permutation mode)Each replicate permutes the treatment column within the analyst's actual dataset. This preserves the observed covariate and outcome distributions and is the natural H0 reference for a real analysis. Requires the
treat.namecolumn to be present indf.analysis.
Permutation mode
Under the null hypothesis of no subgroup heterogeneity, the treatment
assignment is exchangeable with the observed outcomes. Permuting
treat.name within each replicate breaks any true treatment-by-
subgroup interaction while preserving the marginal distributions of all
other variables. This is a model-free H0 reference that adapts
automatically to the actual dataset structure.
P1 approximation
The asymptotic per-subgroup \(P_1\) at (c1, c2) is computed
via compute_detection_probability_glm() using the effective
information \(d_{\text{eff}}\) at n.min. The ratio
\(L_{\text{eff}} = \log(1 - \hat{\text{FPR}}) / \log(1 - P_1)\)
is the implied multiplicity factor specific to this threshold pair and
dataset — it replaces the universal power-law model and does not assume
threshold-independence.
Examples
if (FALSE) { # \dontrun{
# ---- Binary example (permutation mode) ----
set.seed(1)
df <- data.frame(
id = 1:500, treat = rbinom(500, 1, 0.5),
bm1 = as.factor(rbinom(500, 1, 0.7)),
bm2 = as.factor(rbinom(500, 1, 0.5)),
age = round(rnorm(500, 55, 10)),
y = rbinom(500, 1, 0.30) # null: no treatment effect
)
fs_params <- list(
confounders.name = c("bm1", "bm2", "age"),
outcome.name = "y", event.name = "y",
treat.name = "treat", id.name = "id",
outcome_type = "binary", effect_measure = "OR",
adverse_outcome = TRUE,
pconsistency.threshold = 0.90, fs.splits = 400,
n.min = 60, maxk = 2,
use_lasso = TRUE, use_grf = TRUE,
is.RCT = TRUE, details = FALSE, quiet = TRUE,
parallel_args = list(plan = "sequential", workers = 1)
)
res <- fpr_calibration(
c1 = 1.70, c2 = 1.70,
df.analysis = df,
fs_params = fs_params,
nsims = 200,
n_min = 60, outcome_type_approx = "binary", p_event = 0.30,
sim_seed = 42, n_workers = 4
)
print(res)
} # }