| Title: | Exploratory Subgroup Identification in Survival and GLM Outcomes |
|---|---|
| Description: | Implements statistical methods for exploratory subgroup identification in clinical trials. Provides tools for identifying patient subgroups with differential treatment effects using machine learning approaches including Generalized Random Forests (GRF), LASSO regularization, and exhaustive combinatorial search algorithms. Supports survival endpoints (Cox proportional hazards), binary outcomes (log odds ratio, log relative risk, risk difference), continuous outcomes (mean difference), and count / rate outcomes (log incidence rate ratio via Poisson, quasi-Poisson, or negative-binomial GLMs with optional person-time offset). Features bootstrap bias correction using infinitesimal jackknife methods to address selection bias in post-hoc analyses. Designed for clinical researchers conducting exploratory subgroup analyses in randomized controlled trials, particularly for multi-regional clinical trials (MRCT) requiring regional consistency evaluation. Methods are described in Leon et al. (2024) <doi:10.1002/sim.10163>. Post-selection inference for the identified subgroup follows León and Anderson (2026) <doi:10.48550/arXiv.2609.38361>. The search methodology is given in León and Anderson (2026) <doi:10.48550/arXiv.2610.28379>. |
| Authors: | Larry Leon [aut, cre] |
| Maintainer: | Larry Leon <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.4.1 |
| Built: | 2026-10-30 09:01:14 UTC |
| Source: | https://github.com/larry-leon/forestsearch |
Covariate set handed to forestsearch()/mrct_region_sims() for the paper's observed vs unobserved analysis cases.
analysis_covariates( dgm, case = c("observed", "unobserved"), drop = character(0) )analysis_covariates( dgm, case = c("observed", "unobserved"), drop = character(0) )
dgm |
An MRCT data-generating mechanism returned by
|
case |
Character. |
drop |
Character vector of covariate names (without the |
Character vector of z_-prefixed covariate column names.
Analyzes a subgroup and returns formatted results for summary table. Uses optimized cox_summary() and reduces redundant calculations.
analyze_subgroup( df_sub, outcome.name, event.name, treat.name, strata.name, subgroup_name, hr_a, potentialOutcome.name, return_medians, N )analyze_subgroup( df_sub, outcome.name, event.name, treat.name, strata.name, subgroup_name, hr_a, potentialOutcome.name, return_medians, N )
df_sub |
Data frame for subgroup. |
outcome.name |
Character. Name of outcome variable. |
event.name |
Character. Name of event indicator variable. |
treat.name |
Character. Name of treatment variable. |
strata.name |
Character. Name of strata variable (optional). |
subgroup_name |
Character. Subgroup name. |
hr_a |
Character. Adjusted hazard ratio (optional). |
potentialOutcome.name |
Character. Name of potential outcome variable (optional). |
return_medians |
Logical. Use medians or RMST. |
N |
Integer. Total sample size. |
Character vector of results.
## Not run: library(survival) df <- data.frame( tte = gbsg$rfstime / 30.4375, event = gbsg$status, treat = gbsg$hormon ) analyze_subgroup( df_sub = df, outcome.name = "tte", event.name = "event", treat.name = "treat", strata.name = NULL, subgroup_name = "All", hr_a = NA, potentialOutcome.name = NULL, return_medians = TRUE, N = nrow(df) ) ## End(Not run)## Not run: library(survival) df <- data.frame( tte = gbsg$rfstime / 30.4375, event = gbsg$status, treat = gbsg$hormon ) analyze_subgroup( df_sub = df, outcome.name = "tte", event.name = "event", treat.name = "treat", strata.name = NULL, subgroup_name = "All", hr_a = NA, potentialOutcome.name = NULL, return_medians = TRUE, N = nrow(df) ) ## End(Not run)
Describes the size-matched random benchmark subgroups regenerated in
every simulated trial. With the default nested = TRUE, a single
index draw of max(sizes) patients is taken per trial and the smaller
benchmarks are its prefixes (random15 random20
random40 random60), exactly as in the
extreme-subgroups vignettes. Membership columns are named
paste0(prefix, sizes) and take values 0/1.
benchmark_spec(sizes = c(60L, 40L, 20L, 15L), nested = TRUE, prefix = "random")benchmark_spec(sizes = c(60L, 40L, 20L, 15L), nested = TRUE, prefix = "random")
sizes |
Integer vector of benchmark sizes. Order determines the
column-creation order only; the draw uses |
nested |
Logical. |
prefix |
Column-name prefix, default |
An object of class "benchmark_spec".
benchmark_spec() benchmark_spec(sizes = c(100, 50), prefix = "rnd")benchmark_spec() benchmark_spec(sizes = c(100, 50), prefix = "rnd")
Runs bootstrap analysis for ForestSearch, fitting Cox models and computing bias-corrected estimates and valid CIs (see vignette for references)
bootstrap_results( fs.est, df_boot_analysis, cox.formula.boot, nb_boots, show_three, H_obs, Hc_obs, seed = 8316951L, estimator_fn = NULL, effect_measure = NULL, boot_index_mat = NULL, mr_in_replicates = FALSE )bootstrap_results( fs.est, df_boot_analysis, cox.formula.boot, nb_boots, show_three, H_obs, Hc_obs, seed = 8316951L, estimator_fn = NULL, effect_measure = NULL, boot_index_mat = NULL, mr_in_replicates = FALSE )
fs.est |
List. ForestSearch results object from
|
df_boot_analysis |
Data frame. Bootstrap analysis data with same structure
as |
cox.formula.boot |
Formula. Cox model formula for bootstrap, typically
created by |
nb_boots |
Integer. Number of bootstrap samples to generate (e.g., 500-1000). More iterations provide better bias correction but increase computation time. |
show_three |
Logical. If |
H_obs |
Numeric. Observed log hazard ratio for subgroup H (harm/questionable group,
|
Hc_obs |
Numeric. Observed log hazard ratio for subgroup H^c (complement/recommend,
|
seed |
Integer. Random seed for reproducibility. Default 8316951L.
Only used when |
estimator_fn |
Closure or |
effect_measure |
Character or |
boot_index_mat |
Integer matrix or |
mr_in_replicates |
Logical. Whether multiplier resampling runs inside
each replicate. Default |
Data.table with one row per bootstrap iteration and columns:
Integer. Bootstrap iteration number (1 to nb_boots)
Bias-corrected estimate for H using method 1:
H_obs - (Hstar_star - Hstar_obs)
Bias-corrected estimate for H using method 2:
2*H_obs - (H_star + Hstar_star - Hstar_obs)
Bias-corrected estimate for H^c using method 1
Bias-corrected estimate for H^c using method 2
Numeric. Maximum subgroup hazard ratio found
Integer. Number of candidate factors evaluated
Integer. Maximum number of factor combinations
Integer. Number of events in control arm of original subgroup H on bootstrap sample
Integer. Number of events in treatment arm of original subgroup H on bootstrap sample
Integer. Number of events in control arm of original subgroup H^c on bootstrap sample
Integer. Number of events in treatment arm of original subgroup H^c on bootstrap sample
Integer. Number of events in control arm of new subgroup H* on original data
Integer. Number of events in treatment arm of new subgroup H* on original data
Integer. Number of events in control arm of new subgroup H^c* on original data
Integer. Number of events in treatment arm of new subgroup H^c* on original data
Numeric. Minutes spent on subgroup search in this iteration
Numeric. Total minutes for this bootstrap iteration
Numeric. Consistency p-value for top subgroup
Integer 0/1. Identification flag; equivalent to
as.integer(!is.na(Pcons)). Added in v0.2.0 to mirror the
fold_summary$any_found column produced by
forestsearch_tenfold, so cross-API diagnostics
(sum(results$any_found == 1L)) work identically for
bootstrap and CV outputs.
Numeric. Hazard ratio for top subgroup
Integer. Sample size of top subgroup
Integer. Number of events in top subgroup
Integer. Number of factors defining top subgroup
Numeric. Subgroup group ID
Numeric. Subgroup index
Character. First factor label
Character. Second factor label
Character. Third factor label
Character. Fourth factor label
Character. Fifth factor label
Character. Sixth factor label
Character. Seventh factor label
Character. GRF policy-tree cut expressions
returned on this bootstrap sample, collapsed with " | " when
multiple cuts are produced. NA_character_ when GRF was
not used, the bootstrap forestsearch call errored, or GRF
returned no cuts. Populated independently of whether a
subgroup was identified: GRF may surface a candidate cut on
bootstraps where the downstream consistency stage rejects all
candidates. Mirrors the grf_cuts column that
forestsearch_tenfold captures in its
fold_summary return slot.
Rows where no valid subgroup was found will have NA for bias corrections.
The returned object has a "timing" attribute with summary statistics.
Two bias correction approaches are implemented:
Method 1 (Simple Optimism):
where is the new subgroup HR on bootstrap data and
is the new subgroup HR on original data.
Method 2 (Double Bootstrap):
where is the original subgroup HR on bootstrap data.
where:
H_obs: Original subgroup HR on original data
H_star: Original subgroup HR on bootstrap data
Hstar_obs: New subgroup (found in bootstrap) HR on original data
Hstar_star: New subgroup (found in bootstrap) HR on bootstrap data
Uses doFuture backend for parallel execution (configured externally)
Sets reproducible seeds: 8316951 + boot * 100 for each iteration
Each bootstrap iteration runs full ForestSearch pipeline including variable selection, subgroup search, and consistency evaluation
Sequential execution within each bootstrap prevents nested parallelization
Failed bootstrap iterations generate warnings but don't stop execution
Confounders are removed from bootstrap data to force fresh variable selection
Each bootstrap iteration modifies ForestSearch arguments to:
Suppress output: details, show_candidate_summary,
plot.sg, plot.grf all set to FALSE
Force re-selection: grf_res and grf_cuts set to NULL
Prevent nested parallel: parallel_args$plan = "sequential",
workers = 1
Typical runtime: 1-5 seconds per bootstrap iteration
For 1000 bootstraps with 6 workers: ~3-10 minutes total
Memory usage scales with dataset size and number of workers
Consider reducing nb_boots for initial testing (e.g., 100)
The function gracefully handles three failure modes:
Bootstrap sample creation fails: Returns row with all NA
ForestSearch fails to run: Warns and returns row with all NA
ForestSearch runs but finds no subgroup: Returns row with all NA
All three cases ensure the foreach loop can still combine results via rbind.
This function is designed to be called within a foreach loop
with %dofuture% operator. It requires:
All functions in get_bootstrap_exports to be available
in the parallel workers
Packages listed in BOOTSTRAP_REQUIRED_PACKAGES to be installed
Proper parallel backend setup via setup_parallel_SGcons
forestsearch_bootstrap_dofuture for the wrapper function that
sets up parallelization and calls this function
build_cox_formula for creating the Cox formula
fit_cox_models for initial Cox model fitting
get_Cox_sg for Cox model fitting on subgroups
get_dfRes for processing bootstrap results into confidence intervals
bootstrap_ystar for generating the Ystar matrix
## Not run: # Typically called via forestsearch_bootstrap_dofuture() # Manual usage for debugging: # 1. Fit initial ForestSearch model fs_result <- forestsearch( df.analysis = mydata, outcome.name = "time", event.name = "status", treat.name = "treatment", confounders.name = c("age", "sex", "stage") ) # 2. Build Cox formula cox_formula <- build_cox_formula("time", "status", "treatment") # 3. Get observed estimates cox_fits <- fit_cox_models(fs_result$df.est, cox_formula) # 4. Set up parallel backend library(doFuture) plan(multisession, workers = 6) # 5. Run bootstrap (note: this is already parallelized internally) boot_results <- bootstrap_results( fs.est = fs_result, df_boot_analysis = fs_result$df.est, cox.formula.boot = cox_formula, nb_boots = 100, show_three = TRUE, H_obs = cox_fits$H_obs, Hc_obs = cox_fits$Hc_obs ) # 6. Check results summary(boot_results) # Proportion of bootstraps that found a subgroup mean(!is.na(boot_results$H_biasadj_2)) ## End(Not run)## Not run: # Typically called via forestsearch_bootstrap_dofuture() # Manual usage for debugging: # 1. Fit initial ForestSearch model fs_result <- forestsearch( df.analysis = mydata, outcome.name = "time", event.name = "status", treat.name = "treatment", confounders.name = c("age", "sex", "stage") ) # 2. Build Cox formula cox_formula <- build_cox_formula("time", "status", "treatment") # 3. Get observed estimates cox_fits <- fit_cox_models(fs_result$df.est, cox_formula) # 4. Set up parallel backend library(doFuture) plan(multisession, workers = 6) # 5. Run bootstrap (note: this is already parallelized internally) boot_results <- bootstrap_results( fs.est = fs_result, df_boot_analysis = fs_result$df.est, cox.formula.boot = cox_formula, nb_boots = 100, show_three = TRUE, H_obs = cox_fits$H_obs, Hc_obs = cox_fits$Hc_obs ) # 6. Check results summary(boot_results) # Proportion of bootstraps that found a subgroup mean(!is.na(boot_results$H_biasadj_2)) ## End(Not run)
Returns the integer count matrix consumed by the
infinitesimal-jackknife variance estimator in get_dfRes.
Entry is the number of times row of df was
selected on bootstrap iteration .
bootstrap_ystar(df, nb_boots, seed = 8316951L)bootstrap_ystar(df, nb_boots, seed = 8316951L)
df |
Data frame. Counts are computed by positional row index;
|
nb_boots |
Integer. Number of bootstrap samples. |
seed |
Integer or |
Integer matrix with nb_boots rows and nrow(df)
columns.
This function previously generated the count matrix via a parallel
foreach pass, with counts derived by matching against a
user-supplied id column. It now runs on the main process and
counts by positional row index via tabulate,
matching how bootstrap_results constructs bootstrap data
(df[in_boot, ]). For data with unique subject ids (the standard
clinical-trial case) the two implementations are equivalent. The new
implementation is also robust to non-unique id values.
Bootstrap indices generated by this version are not bit-identical to those produced by v0.1.x for the same seed (the generator now uses the main-process Mersenne-Twister stream rather than per-task L'Ecuyer streams). Statistical properties of bootstrap summaries are preserved.
## Not run: df <- data.frame(id = 1:50, tte = rexp(50), event = rbinom(50, 1, 0.6), treat = rep(0:1, 25)) ystar <- bootstrap_ystar(df, nb_boots = 10) dim(ystar) # 10 x 50 rowSums(ystar) # all equal to 50 ## End(Not run)## Not run: df <- data.frame(id = 1:50, tte = rexp(50), event = rbinom(50, 1, 0.6), treat = rep(0:1, 25)) ystar <- bootstrap_ystar(df, nb_boots = 10) dim(ystar) # 10 x 50 rowSums(ystar) # all equal to 50 ## End(Not run)
Constructs a publication-quality gt table summarizing subgroup
identification and classification rates across one or more data generation
scenarios and analysis methods. The layout mirrors Table 4 of
Leon et al. (2024) with metrics grouped by model scenario (null / alt)
and columns for each analysis method.
build_classification_table( scenario_results, analyses = NULL, digits = 2, title = "Subgroup Identification and Classification Rates", n_sims = NULL, bold_threshold = 0.05, font_size = 12, subgroup_notation = c("harm", "benefit") )build_classification_table( scenario_results, analyses = NULL, digits = 2, title = "Subgroup Identification and Classification Rates", n_sims = NULL, bold_threshold = 0.05, font_size = 12, subgroup_notation = c("harm", "benefit") )
scenario_results |
Named list. Each element is itself a list with:
|
analyses |
Character vector of analysis labels to include
(e.g., |
digits |
Integer. Decimal places for proportions. Default: 2. |
title |
Character. Table title. Default:
|
n_sims |
Integer. Number of simulations (for subtitle). Default:
|
bold_threshold |
Numeric. Type I error threshold above which the
|
font_size |
Numeric. Font size in pixels for table text. Default: 12. Increase to 14 or 16 for larger display. |
subgroup_notation |
Character. |
For each scenario the function computes:
any(H): Proportion of simulations identifying any subgroup.
sens(H): Mean sensitivity (only under alternative).
sens(Hc): Mean specificity.
ppv(H): Mean positive predictive value (only under
alternative).
ppv(Hc): Mean negative predictive value.
avg|H|: Mean size of identified subgroup (when found).
Under the null hypothesis the rows are reduced to any(H),
sens(Hc), ppv(Hc), and avg|H|.
A gt table object.
format_oc_results,
summarize_simulation_results
## Not run: # Assemble results from H0 and H1 simulations scenarios <- list( null = list( results = results_null, label = "M1", n_sample = 700, dgm = dgm_null, hypothesis = "null" ), alt = list( results = results_alt, label = "M1", n_sample = 700, dgm = dgm_calibrated, hypothesis = "alt" ) ) build_classification_table(scenarios, n_sims = 100) ## End(Not run)## Not run: # Assemble results from H0 and H1 simulations scenarios <- list( null = list( results = results_null, label = "M1", n_sample = 700, dgm = dgm_null, hypothesis = "null" ), alt = list( results = results_alt, label = "M1", n_sample = 700, dgm = dgm_calibrated, hypothesis = "alt" ) ) build_classification_table(scenarios, n_sims = 100) ## End(Not run)
Constructs a Cox model formula from variable names, optionally adjusted for additional covariates.
build_cox_formula( outcome.name, event.name, treat.name, adjust_covariates = NULL )build_cox_formula( outcome.name, event.name, treat.name, adjust_covariates = NULL )
outcome.name |
Character. Name of outcome variable. |
event.name |
Character. Name of event indicator variable. |
treat.name |
Character. Name of treatment variable. |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
An R formula object for Cox regression. When adjust_covariates
is supplied, the treatment term is always placed first on the
right-hand side.
build_cox_formula("time_months", "event", "treat") build_cox_formula("time_months", "event", "treat", adjust_covariates = c("strata(site)", "age"))build_cox_formula("time_months", "event", "treat") build_cox_formula("time_months", "event", "treat", adjust_covariates = c("strata(site)", "age"))
Constructs a publication-quality gt table summarizing estimation
properties for hazard ratios in the identified subgroup and its complement.
The layout mirrors Table 5 of Leon et al. (2024), showing average estimate,
empirical SD, min, max, and relative bias for each estimator.
build_estimation_table( results, dgm, analysis_method = "FSlg", n_boots = NULL, digits = 2, title = "Estimation Properties", subtitle = NULL, font_size = 12, cde_H = NULL, cde_Hc = NULL, subgroup_notation = c("harm", "benefit"), trim_threshold = 1000, trim_fraction = 0.01 )build_estimation_table( results, dgm, analysis_method = "FSlg", n_boots = NULL, digits = 2, title = "Estimation Properties", subtitle = NULL, font_size = 12, cde_H = NULL, cde_Hc = NULL, subgroup_notation = c("harm", "benefit"), trim_threshold = 1000, trim_fraction = 0.01 )
results |
|
dgm |
DGM object. Used for true parameter values ( |
analysis_method |
Character. Which analysis method to tabulate
(e.g., |
n_boots |
Integer or |
digits |
Integer. Decimal places. Default: 2. |
title |
Character. Table title. |
subtitle |
Character or |
font_size |
Numeric. Font size in pixels for table text. Default: 12. Increase to 14 or 16 for larger display. |
cde_H |
Numeric or |
cde_Hc |
Numeric or |
subgroup_notation |
Character. |
trim_threshold |
Numeric or |
trim_fraction |
Numeric in |
Uses the paper's notation conventions:
theta-dagger: Marginal (causal) HR truth
theta-ddagger: Controlled direct effect (CDE) truth
theta-hat(H-hat): Plugin Cox estimate in identified subgroup
theta-hat*(H-hat): Bootstrap bias-corrected estimate
Includes both Cox-based HR and AHR (Average Hazard Ratio from loghr_po) estimators when AHR columns are present in the results.
For each subgroup (H and Hc) the function reports:
Avg: Mean of the estimates across estimable simulations.
SD: Empirical standard deviation.
Min / Max: Range.
b-dagger: Relative bias (percent) vs marginal truth,
100 * (Avg - theta_dagger) / theta_dagger.
b-ddagger (conditional): Relative bias (percent) vs CDE truth, shown when CDE values are available.
When bootstrap-corrected columns (hr.H.bc, hr.Hc.bc) are
present in results, an additional bias-corrected row
(theta-hat*(H-hat)) is added per subgroup.
When AHR columns (ahr.H.hat, ahr.Hc.hat) are present, AHR
estimation rows are appended using the DGM's true AHR values for relative
bias calculation.
When CDE columns (cde.H.hat, cde.Hc.hat) are present and
CDE truth values are available, CDE estimation rows
(theta-ddagger(H-hat)) are appended. The b-dagger column for CDE rows
reports bias relative to the CDE truth rather than the marginal HR.
A gt table object, or NULL if no estimable
realizations exist.
build_classification_table,
format_oc_results, get_dgm_hr
## Not run: # Basic usage (marginal truth only) build_estimation_table( results = results_alt, dgm = dgm_calibrated, analysis_method = "FSlg" ) # With CDE truth (full Table 5 alignment) build_estimation_table( results = results_alt, dgm = dgm_calibrated, cde_H = 2.25, cde_Hc = 0.60 ) ## End(Not run)## Not run: # Basic usage (marginal truth only) build_estimation_table( results = results_alt, dgm = dgm_calibrated, analysis_method = "FSlg" ) # With CDE truth (full Table 5 alignment) build_estimation_table( results = results_alt, dgm = dgm_calibrated, cde_H = 2.25, cde_Hc = 0.60 ) ## End(Not run)
Uses root-finding to select a value of cens_adjust for
simulate_from_dgm such that a chosen censoring summary
statistic in the simulated data matches the corresponding statistic from
the DGM reference data (dgm$df_super).
calibrate_cens_adjust( dgm, target = c("rate", "km_median"), n = 1000, rand_ratio = 1, analysis_time = 48, max_entry = 24, seed = 42, interval = c(-3, 3), tol = 1e-04, tol_rel = 2.5, n_eval = 2000, verbose = TRUE, ... )calibrate_cens_adjust( dgm, target = c("rate", "km_median"), n = 1000, rand_ratio = 1, analysis_time = 48, max_entry = 24, seed = 42, interval = c(-3, 3), tol = 1e-04, tol_rel = 2.5, n_eval = 2000, verbose = TRUE, ... )
dgm |
An |
target |
Character. Calibration target: |
n |
Integer. Sample size passed to |
rand_ratio |
Numeric. Randomisation ratio passed to
|
analysis_time |
Numeric. Calendar analysis time passed to
|
max_entry |
Numeric. Maximum staggered entry time passed to
|
seed |
Integer. Base random seed. Each evaluation of the objective
function uses this seed for reproducibility. Default |
interval |
Numeric vector of length 2. Search interval for
|
tol |
Numeric. Root-finding tolerance. Default |
tol_rel |
Numeric. Maximum tolerated relative error, in percent; the
call stops if the achieved metric is not within |
n_eval |
Integer. Sample size used inside the objective function
during root-finding. Smaller values are faster but noisier; increase
for precision. Default |
verbose |
Logical. Print search progress and final result.
Default |
... |
Additional arguments passed to |
Two calibration targets are supported:
"rate"Overall censoring rate (proportion censored).
Finds cens_adjust such that
mean(event_sim == 0) in simulated data equals
mean(event == 0) in dgm$df_super.
"km_median"KM-based median censoring time, estimated by
reversing the event indicator so censored observations become the
"event" of interest. Finds cens_adjust such that the
simulated KM median matches the reference KM median.
At each candidate cens_adjust value, the objective function:
Calls simulate_from_dgm() with n = n_eval and
the candidate cens_adjust.
Calls check_censoring_dgm() with verbose = FALSE
to extract the target metric.
Returns sim_metric - ref_metric.
uniroot finds the zero crossing, i.e. the cens_adjust at
which simulated and reference metrics are equal.
The objective is monotone in cens_adjust for both targets:
Larger cens_adjust → longer censoring times →
lower censoring rate and higher KM median.
Smaller cens_adjust → shorter censoring times →
higher censoring rate and lower KM median.
If uniroot fails (the target lies outside the search interval),
the boundary values are printed and a wider interval should be
tried.
Because the objective function involves simulation, there is Monte Carlo
noise. Setting a fixed seed and a sufficiently large n_eval
(>= 2000) reduces noise enough for reliable root-finding. The
tol argument controls the root-finding tolerance on the
cens_adjust scale (not the metric scale).
A named list with elements:
cens_adjustCalibrated cens_adjust value.
targetCalibration target used.
ref_valueReference metric value from dgm$df_super.
sim_valueAchieved metric value in simulated data at the
calibrated cens_adjust.
residualAbsolute difference between sim_value and
ref_value.
iterationsNumber of uniroot iterations.
diagnosticOutput of check_censoring_dgm at
the calibrated value (invisibly).
simulate_from_dgm, check_censoring_dgm,
generate_aft_dgm_flex
## Not run: library(survival) # Build DGM on months scale gbsg$time_months <- gbsg$rfstime / 30.4375 dgm <- generate_aft_dgm_flex( data = gbsg, continuous_vars = c("age", "size", "nodes", "pgr", "er"), factor_vars = c("meno", "grade"), outcome_var = "time_months", event_var = "status", treatment_var = "hormon", subgroup_vars = c("er", "meno"), subgroup_cuts = list(er = 20, meno = 0) ) # Calibrate so simulated censoring rate matches reference cal_rate <- calibrate_cens_adjust( dgm = dgm, target = "rate", n = 1000, analysis_time = 84, max_entry = 24 ) cat("Calibrated cens_adjust (rate):", cal_rate$cens_adjust, "\n") # Calibrate to KM median censoring time instead cal_km <- calibrate_cens_adjust( dgm = dgm, target = "km_median", n = 1000, analysis_time = 84, max_entry = 24 ) cat("Calibrated cens_adjust (km_median):", cal_km$cens_adjust, "\n") # Use calibrated value in simulation sim <- simulate_from_dgm( dgm = dgm, n = 1000, analysis_time = 84, max_entry = 24, cens_adjust = cal_rate$cens_adjust, seed = 123 ) mean(sim$event_sim) # event rate mean(sim$event_sim == 0) # censoring rate — should match ref ## End(Not run)## Not run: library(survival) # Build DGM on months scale gbsg$time_months <- gbsg$rfstime / 30.4375 dgm <- generate_aft_dgm_flex( data = gbsg, continuous_vars = c("age", "size", "nodes", "pgr", "er"), factor_vars = c("meno", "grade"), outcome_var = "time_months", event_var = "status", treatment_var = "hormon", subgroup_vars = c("er", "meno"), subgroup_cuts = list(er = 20, meno = 0) ) # Calibrate so simulated censoring rate matches reference cal_rate <- calibrate_cens_adjust( dgm = dgm, target = "rate", n = 1000, analysis_time = 84, max_entry = 24 ) cat("Calibrated cens_adjust (rate):", cal_rate$cens_adjust, "\n") # Calibrate to KM median censoring time instead cal_km <- calibrate_cens_adjust( dgm = dgm, target = "km_median", n = 1000, analysis_time = 84, max_entry = 24 ) cat("Calibrated cens_adjust (km_median):", cal_km$cens_adjust, "\n") # Use calibrated value in simulation sim <- simulate_from_dgm( dgm = dgm, n = 1000, analysis_time = 84, max_entry = 24, cens_adjust = cal_rate$cens_adjust, seed = 123 ) mean(sim$event_sim) # event rate mean(sim$event_sim == 0) # censoring rate — should match ref ## End(Not run)
Searches over a grid of interaction multipliers (k_inter) to find
the value that produces a target effect size in the subgroup .
Returns a modified "glm_dgm" object with the calibrated
interaction and updated true effects.
calibrate_glm_interaction( data, factor_vars, continuous_vars = NULL, outcome_var, treatment_var, target_effect, outcome_type = c("binary", "continuous", "count"), effect_measure = NULL, offset_var = NULL, subgroup_vars = NULL, subgroup_cuts = NULL, k_treat = 1, adverse_outcome = FALSE, k_inter_range = c(-10, 10), grid_step = 0.05, tol_rel = 2.5, n_super = 5000L, seed = 8316951L, verbose = FALSE )calibrate_glm_interaction( data, factor_vars, continuous_vars = NULL, outcome_var, treatment_var, target_effect, outcome_type = c("binary", "continuous", "count"), effect_measure = NULL, offset_var = NULL, subgroup_vars = NULL, subgroup_cuts = NULL, k_treat = 1, adverse_outcome = FALSE, k_inter_range = c(-10, 10), grid_step = 0.05, tol_rel = 2.5, n_super = 5000L, seed = 8316951L, verbose = FALSE )
data |
The source data frame (same as passed to
|
factor_vars |
Character vector of factor variable names. |
continuous_vars |
Character vector of continuous prognostic variable
names. Passed through to |
outcome_var |
Character string naming the outcome variable. |
treatment_var |
Character string naming the treatment variable. |
target_effect |
Numeric. Target effect size in Q on the scale
determined by |
outcome_type |
Character. One of |
effect_measure |
Character. Effect measure. Default |
offset_var |
Character or |
subgroup_vars |
Character vector of subgroup-defining variables. |
subgroup_cuts |
Named list of cutpoint specifications. |
k_treat |
Numeric. Treatment effect scaling factor passed to
|
adverse_outcome |
Logical. When |
k_inter_range |
Numeric vector of length 2. Search range for
|
grid_step |
Numeric. Grid resolution. Default |
tol_rel |
Numeric. Maximum tolerated relative error, in percent; the
call stops (fails loudly) if the achieved OR is not within |
n_super |
Integer. Super-population size. Default |
seed |
Integer. Random seed. Default |
verbose |
Logical. Print calibration progress. Default |
The function constructs a grid of k_inter values, calls
generate_glm_dgm for each, and selects the value
whose subgroup effect is closest to target_effect.
For binary outcomes with effect_measure = "OR", the target
is on the switched-treatment OR scale (OR > 1 = treatment increases
the outcome).
An object of class "glm_dgm" with the calibrated
k_inter and updated hazard_ratios.
generate_glm_dgm,
simulate_from_glm_dgm
## Not run: dgm <- calibrate_glm_interaction( data = actg_df, factor_vars = paste0("z", 1:12), outcome_var = "y_binary", treatment_var = "treat", target_effect = 2.0, outcome_type = "binary", subgroup_vars = c("z1", "z2"), subgroup_cuts = list(z1 = 1L, z2 = 1L), verbose = TRUE ) print(dgm) ## End(Not run)## Not run: dgm <- calibrate_glm_interaction( data = actg_df, factor_vars = paste0("z", 1:12), outcome_var = "y_binary", treatment_var = "treat", target_effect = 2.0, outcome_type = "binary", subgroup_vars = c("z1", "z2"), subgroup_cuts = list(z1 = 1L, z2 = 1L), verbose = TRUE ) print(dgm) ## End(Not run)
Finds the interaction effect multiplier (k_inter) that achieves a target hazard ratio in the harm subgroup.
calibrate_k_inter( target_hr_harm, model = "alt", k_treat = 1, cens_type = "weibull", k_inter_range = c(-100, 100), tol = 1e-06, tol_rel = 2.5, use_ahr = FALSE, verbose = FALSE, ... )calibrate_k_inter( target_hr_harm, model = "alt", k_treat = 1, cens_type = "weibull", k_inter_range = c(-100, 100), tol = 1e-06, tol_rel = 2.5, use_ahr = FALSE, verbose = FALSE, ... )
target_hr_harm |
Numeric. Target hazard ratio for the harm subgroup |
model |
Character. Model type ("alt" only). Default: "alt" |
k_treat |
Numeric. Treatment effect multiplier. Default: 1 |
cens_type |
Character. Censoring type. Default: "weibull" |
k_inter_range |
Numeric vector of length 2. Search range for k_inter. Default: c(-100, 100) |
tol |
Numeric. Tolerance for root finding. Default: 1e-6 |
tol_rel |
Numeric. Maximum tolerated relative error, in percent; the
call stops if the achieved HR is not within |
use_ahr |
Logical. If TRUE, calibrate to AHR instead of Cox-based HR. Default: FALSE |
verbose |
Logical. Print diagnostic information. Default: FALSE |
... |
Additional arguments passed to |
This function uses uniroot to find the k_inter value such that
the empirical HR (or AHR) in the harm subgroup equals target_hr_harm.
Numeric value of k_inter that achieves the target HR
## Not run: # Find k_inter for HR = 1.5 in harm subgroup k <- calibrate_k_inter(target_hr_harm = 1.5, verbose = TRUE) # Verify dgm <- create_gbsg_dgm(model = "alt", k_inter = k, verbose = TRUE) print(dgm$hr_H_true) # Should be close to 1.5 # Calibrate to AHR instead k_ahr <- calibrate_k_inter(target_hr_harm = 1.5, use_ahr = TRUE, verbose = TRUE) dgm_ahr <- create_gbsg_dgm(model = "alt", k_inter = k_ahr, verbose = TRUE) print(dgm_ahr$AHR_H_true) # Should be close to 1.5 ## End(Not run)## Not run: # Find k_inter for HR = 1.5 in harm subgroup k <- calibrate_k_inter(target_hr_harm = 1.5, verbose = TRUE) # Verify dgm <- create_gbsg_dgm(model = "alt", k_inter = k, verbose = TRUE) print(dgm$hr_H_true) # Should be close to 1.5 # Calibrate to AHR instead k_ahr <- calibrate_k_inter(target_hr_harm = 1.5, use_ahr = TRUE, verbose = TRUE) dgm_ahr <- create_gbsg_dgm(model = "alt", k_inter = k_ahr, verbose = TRUE) print(dgm_ahr$AHR_H_true) # Should be close to 1.5 ## End(Not run)
Finds the treatment effect multiplier (k_treat) that achieves a
target overall hazard ratio in a DGM built by
generate_aft_dgm_flex. The root is found by uniroot
over k_treat with every other argument to
generate_aft_dgm_flex() held fixed.
calibrate_k_treat( target_hr_overall, base_args, k_treat_range = c(-5, 5), tol = 1e-06, tol_rel = 2.5, use_ahr = FALSE, verbose = FALSE )calibrate_k_treat( target_hr_overall, base_args, k_treat_range = c(-5, 5), tol = 1e-06, tol_rel = 2.5, use_ahr = FALSE, verbose = FALSE )
target_hr_overall |
Numeric. Target overall hazard ratio (must be positive). |
base_args |
Named list of arguments to
|
k_treat_range |
Numeric vector of length 2. Search range for
|
tol |
Numeric. Tolerance for root finding. Default |
tol_rel |
Numeric. Maximum tolerated relative error, in percent; the
call stops if the achieved HR is not within |
use_ahr |
Logical. If |
verbose |
Logical. Print diagnostic information. Default
|
This is the natural sibling of calibrate_k_inter (which
targets the harm-subgroup HR under model = "alt") and
calibrate_cens_adjust (which targets the censoring rate).
It is typically used with model = "null" to fix the overall HR
for a uniform-effect DGM, but also works with model = "alt"
when the marginal HR is the calibration target rather than the
subgroup-specific HR.
Reproducibility requires the uniroot objective to be deterministic
in k_treat. This is achieved by including an integer
seed field in base_args so that every call to
generate_aft_dgm_flex() during the search uses the same
random-number stream. If base_args$seed is NULL the
objective becomes stochastic and uniroot may not converge; the
function emits a warning in this case.
Numeric scalar. The calibrated k_treat value. Stops with an
error if the target cannot be reached – the search interval fails to
bracket it even after automatic extension, or the achieved HR lies outside
tol_rel percent of the target.
calibrate_k_inter,
calibrate_cens_adjust,
generate_aft_dgm_flex
## Not run: library(survival) data(gbsg) gbsg$time_months <- gbsg$rfstime / 30.4375 base_args <- list( data = gbsg, continuous_vars = c("age", "size", "nodes", "pgr", "er"), factor_vars = c("meno", "grade"), outcome_var = "time_months", event_var = "status", treatment_var = "hormon", model = "null", n_super = 5000, seed = 99, verbose = FALSE ) # Calibrate k_treat so that the overall HR is exactly 0.70 k <- calibrate_k_treat(target_hr_overall = 0.70, base_args = base_args, verbose = TRUE) # Verify dgm <- do.call(generate_aft_dgm_flex, c(base_args, list(k_treat = k))) print(dgm$hazard_ratios$overall) # ~ 0.70 # Calibrate to AHR instead k_ahr <- calibrate_k_treat(target_hr_overall = 0.70, base_args = base_args, use_ahr = TRUE) dgm_ahr <- do.call(generate_aft_dgm_flex, c(base_args, list(k_treat = k_ahr))) print(dgm_ahr$hazard_ratios$AHR) # ~ 0.70 ## End(Not run)## Not run: library(survival) data(gbsg) gbsg$time_months <- gbsg$rfstime / 30.4375 base_args <- list( data = gbsg, continuous_vars = c("age", "size", "nodes", "pgr", "er"), factor_vars = c("meno", "grade"), outcome_var = "time_months", event_var = "status", treatment_var = "hormon", model = "null", n_super = 5000, seed = 99, verbose = FALSE ) # Calibrate k_treat so that the overall HR is exactly 0.70 k <- calibrate_k_treat(target_hr_overall = 0.70, base_args = base_args, verbose = TRUE) # Verify dgm <- do.call(generate_aft_dgm_flex, c(base_args, list(k_treat = k))) print(dgm$hazard_ratios$overall) # ~ 0.70 # Calibrate to AHR instead k_ahr <- calibrate_k_treat(target_hr_overall = 0.70, base_args = base_args, use_ahr = TRUE) dgm_ahr <- do.call(generate_aft_dgm_flex, c(base_args, list(k_treat = k_ahr))) print(dgm_ahr$hazard_ratios$AHR) # ~ 0.70 ## End(Not run)
Given per-subgroup approximation probabilities and simulated FPR
values at multiple sample sizes, fits the power-law model
.
calibrate_L_eff(N, P1, sim_fpr, n_min = 60)calibrate_L_eff(N, P1, sim_fpr, n_min = 60)
N |
Integer vector. Sample sizes at which simulations were run. |
P1 |
Numeric vector. Per-subgroup detection probability from
|
sim_fpr |
Numeric vector. Simulated procedure-level FPR at each N. |
n_min |
Integer. Minimum subgroup size used in ForestSearch. Default: 60. |
At each sample size, the implied is:
A log-linear model is then fit:
.
A list of class "leff_calibration" with components:
Numeric. Fitted constant.
Numeric. Fitted power-law exponent.
Integer. Minimum subgroup size.
Numeric. R-squared of the log-linear fit.
Data frame with N, P1, sim_fpr, L_eff, and fitted values.
The lm object.
# From binary threshold calibration results cal <- calibrate_L_eff( N = c(200, 500, 700, 1000), P1 = c(0.152, 0.109, 0.091, 0.071), sim_fpr = c(0.185, 0.220, 0.270, 0.280), n_min = 60 ) print(cal)# From binary threshold calibration results cal <- calibrate_L_eff( N = c(200, 500, 700, 1000), P1 = c(0.152, 0.109, 0.091, 0.071), sim_fpr = c(0.185, 0.220, 0.270, 0.280), n_min = 60 ) print(cal)
Compares the censoring distribution observed in the data used to build the
DGM against the censoring generated by simulate_from_dgm.
Reports censoring rates, time quantiles, KM-based median censoring times,
and flags substantial discrepancies.
check_censoring_dgm( sim_data, dgm, treat_var = "treat_sim", rate_tol = 0.1, median_tol = 0.25, verbose = TRUE )check_censoring_dgm( sim_data, dgm, treat_var = "treat_sim", rate_tol = 0.1, median_tol = 0.25, verbose = TRUE )
sim_data |
A |
dgm |
An |
treat_var |
Character. Name of the treatment column in
|
rate_tol |
Numeric. Absolute tolerance (proportion scale) for
flagging a censoring-rate discrepancy. Default |
median_tol |
Numeric. Relative tolerance for flagging a KM median
censoring-time discrepancy. Default |
verbose |
Logical. If |
The reference censoring distribution is derived from dgm$df_super,
sampled with replacement from the data passed to
generate_aft_dgm_flex(). Columns y (observed time) and
event (event indicator) in df_super reflect the original
observed censoring process on the DGM time scale.
The KM median censoring time is estimated by reversing the event indicator
(1 - event), treating events as censored and censored observations
as the event of interest. This gives a non-parametric estimate of the
censoring time distribution unconfounded by event occurrence.
Common causes of discrepancy: (1) time-scale mismatch (DGM built on days,
analysis_time in months); check exp(dgm$model_params$mu)
against your analysis_time. (2) Large cens_adjust shifting
censoring substantially from the fitted model. (3) Short
analysis_time or time_eos making administrative censoring
dominate the censoring process.
Invisibly returns a named list. Elements are: rates (data
frame of censoring rates overall and by arm); quantiles (data
frame of censoring-time quantiles among censored subjects);
km_medians (data frame of KM-based median censoring times); and
flags (character vector of triggered warnings, empty if none).
simulate_from_dgm, generate_aft_dgm_flex
## Not run: dgm <- setup_gbsg_dgm(model = "null", verbose = FALSE) sim_data <- simulate_from_dgm(dgm, n = 200) check_censoring_dgm(sim_data, dgm = dgm) ## End(Not run)## Not run: dgm <- setup_gbsg_dgm(model = "null", verbose = FALSE) sim_data <- simulate_from_dgm(dgm, n = 200) check_censoring_dgm(sim_data, dgm = dgm) ## End(Not run)
Separates successful results from errors in the output of a
foreach loop run with
.errorhandling = "pass". Successful results are combined via
rbindlist (with an rbind fallback
if data.table is not available) and failed tasks are counted,
reported, and optionally escalated to a hard error.
collect_results( raw_list, label = "", stop_on_all_fail = TRUE, max_error_messages = 3L, keep_diagnostics = FALSE )collect_results( raw_list, label = "", stop_on_all_fail = TRUE, max_error_messages = 3L, keep_diagnostics = FALSE )
raw_list |
List returned by |
label |
Character. Optional label included in the warning
message to identify which simulation phase produced the errors
(e.g. |
stop_on_all_fail |
Logical. If |
max_error_messages |
Integer. Maximum number of distinct
error messages to print in the diagnostic output. Default
|
keep_diagnostics |
Logical. If |
This replaces the fragile pattern of calling nrow() or
rbind() directly on a raw foreach result list, which
fails silently when error objects are mixed in with data-frame rows.
Design rationale for the reporting strategy:
A warning is emitted so that calling code
(and tryCatch / withCallingHandlers blocks) can
capture the failure condition programmatically.
The error summary is also printed with cat to
stderr, because warning and
message output is sometimes buffered or
suppressed inside future/foreach parallel workers or when
options(warn = -1) is set by user code upstream. The
cat() output is the visible-no-matter-what fallback.
When stop_on_all_fail = TRUE (the default), a
completely empty result list is escalated to an error rather
than silently returning an empty data frame. This avoids
downstream code proceeding with zero rows and producing
misleading summaries. Calibration code that may legitimately
tolerate some failures (e.g. Monte Carlo FPR estimation) can
set stop_on_all_fail = FALSE.
The returned object is always a data frame – never NULL –
because attributes (n_failed, n_total) attached to
NULL are silently discarded by R, whereas attributes on an
empty data frame are preserved and inspectable by the caller.
A data frame of row-bound successful results. Always a
data frame; never NULL – an empty data frame is
returned when every task failed and stop_on_all_fail = FALSE.
Two attributes are always attached:
n_failedInteger. Number of tasks that returned errors.
n_totalInteger. Total number of tasks (successes + failures).
When keep_diagnostics = TRUE, a third attribute
"diagnostics" may be attached – a named list of
per-replicate diagnostic objects, keyed by "sim<ID>"
when the source element carries an "sim_id" attribute
(run_simulation_analysis sets this automatically).
data.table::rbindlist() drops per-element attributes during
row-binding. When the caller has populated each replicate with
heavy diagnostic objects (e.g. via
run_simulation_analysis with a non-empty
keep argument), set keep_diagnostics = TRUE to
retain them. The diagnostics attribute on each input element
must carry attr(..., "sim_id") for stable keying; that is
set automatically by run_simulation_analysis().
reset_workers for resetting the
future plan and releasing worker memory between
simulation phases.
library(foreach) library(doFuture) future::plan("sequential") raw <- foreach(i = 1:5, .errorhandling = "pass") %dofuture% { if (i == 3L) stop("simulated failure") data.frame(task = i, value = rnorm(1)) } res <- collect_results(raw, label = "demo") # Warning: demo: 1 of 5 tasks failed. nrow(res) # [1] 4 attr(res, "n_failed") # [1] 1library(foreach) library(doFuture) future::plan("sequential") raw <- foreach(i = 1:5, .errorhandling = "pass") %dofuture% { if (i == 3L) stop("simulated failure") data.frame(task = i, value = rnorm(1)) } res <- collect_results(raw, label = "demo") # Warning: demo: 1 of 5 tasks failed. nrow(res) # [1] 4 attr(res, "n_failed") # [1] 1
Generates and compares detection probability curves for multiple subgroup sample sizes.
compare_detection_curves( n_sg_values, prop_cens = 0.3, hr_threshold = 1.25, hr_consistency = 1, theta_range = c(0.5, 3), n_points = 40L, verbose = TRUE )compare_detection_curves( n_sg_values, prop_cens = 0.3, hr_threshold = 1.25, hr_consistency = 1, theta_range = c(0.5, 3), n_points = 40L, verbose = TRUE )
n_sg_values |
Integer vector. Subgroup sample sizes to compare. |
prop_cens |
Numeric. Proportion censored. Default: 0.3 |
hr_threshold |
Numeric. HR threshold. Default: 1.25 |
hr_consistency |
Numeric. HR consistency threshold. Default: 1.0 |
theta_range |
Numeric vector of length 2. Range of HR values. Default: c(0.5, 3.0) |
n_points |
Integer. Number of points per curve. Default: 40 |
verbose |
Logical. Print progress. Default: TRUE |
A data.frame with all curves combined, including n_sg as a factor.
## Not run: comparison <- compare_detection_curves( n_sg_values = c(40, 60, 80, 100), prop_cens = 0.2 ) # Plot with ggplot2 library(ggplot2) ggplot(comparison, aes(x = theta, y = probability, color = factor(n_sg))) + geom_line(linewidth = 1) + labs(x = "True HR", y = "P(Detect)", color = "n_sg") + theme_minimal() ## End(Not run)## Not run: comparison <- compare_detection_curves( n_sg_values = c(40, 60, 80, 100), prop_cens = 0.2 ) # Plot with ggplot2 library(ggplot2) ggplot(comparison, aes(x = theta, y = probability, color = factor(n_sg))) + geom_line(linewidth = 1) + labs(x = "True HR", y = "P(Detect)", color = "n_sg") + theme_minimal() ## End(Not run)
Cross-tabulates subgroup assignments from two analyses and produces
a formatted gt summary table with concordance statistics.
Accepts forestsearch result objects, GRF result lists, or raw
indicator vectors. Non-detection (no subgroup found) is handled
automatically: all subjects are classified to the complement.
compare_fs_subgroups( fs1, fs2, label1 = "Analysis 1", label2 = "Analysis 2", id.name = NULL, sg0_label = NULL, sg1_label = NULL, subgroup_notation = c("harm", "benefit"), title = "Subgroup Membership Comparison", subtitle = NULL, font_size = 12 )compare_fs_subgroups( fs1, fs2, label1 = "Analysis 1", label2 = "Analysis 2", id.name = NULL, sg0_label = NULL, sg1_label = NULL, subgroup_notation = c("harm", "benefit"), title = "Subgroup Membership Comparison", subtitle = NULL, font_size = 12 )
fs1 |
Either a |
fs2 |
Same types as |
label1 |
Character. Display label for the first analysis. |
label2 |
Character. Display label for the second analysis. |
id.name |
Character. Subject ID column for merge alignment.
Only used when both inputs are result objects.
If |
sg0_label |
Character or |
sg1_label |
Character or |
subgroup_notation |
Character. |
title |
Character. Table title. |
subtitle |
Character or |
font_size |
Numeric. Font size for gt table. |
A list with:
A gt cross-tabulation table.
The raw cross-tabulation matrix.
Agreement, kappa, counts.
Per-subject assignments from both analyses.
## Not run: # Harm search: Cox vs Poisson (both forestsearch objects) result <- compare_fs_subgroups(fs_cox, fs_pois, label1 = "Cox PH", label2 = "Poisson", subgroup_notation = "harm") # Benefit search: FS vs GRF (handles non-detection automatically) result <- compare_fs_subgroups(fs_result, grf_result, label1 = "ForestSearch", label2 = "GRF", subgroup_notation = "benefit") # Raw vectors result <- compare_fs_subgroups( c(rep(0L, 30), rep(1L, 70)), c(rep(0L, 25), rep(1L, 75)), label1 = "Method A", label2 = "Method B") ## End(Not run)## Not run: # Harm search: Cox vs Poisson (both forestsearch objects) result <- compare_fs_subgroups(fs_cox, fs_pois, label1 = "Cox PH", label2 = "Poisson", subgroup_notation = "harm") # Benefit search: FS vs GRF (handles non-detection automatically) result <- compare_fs_subgroups(fs_result, grf_result, label1 = "ForestSearch", label2 = "GRF", subgroup_notation = "benefit") # Raw vectors result <- compare_fs_subgroups( c(rep(0L, 30), rep(1L, 70)), c(rep(0L, 25), rep(1L, 75)), label1 = "Method A", label2 = "Method B") ## End(Not run)
Performs comprehensive comparison of multiple survreg models including convergence checking, information criteria comparison, and model selection.
compare_multiple_survreg( ..., model_names = NULL, verbose = TRUE, criteria = c("AIC", "BIC") )compare_multiple_survreg( ..., model_names = NULL, verbose = TRUE, criteria = c("AIC", "BIC") )
... |
survreg model objects to compare |
model_names |
Optional character vector of model names |
verbose |
Logical, whether to print detailed output (default: TRUE) |
criteria |
Character vector of criteria to use ("AIC", "BIC", or both) |
A list of class "multi_survreg_comparison" containing:
Named list of input models
Convergence status for each model
Model comparison statistics
Model rankings by different criteria
Name of the best model
Text recommendation
## Not run: fit1 <- survreg(Surv(time, status) ~ x, dist = "weibull") fit2 <- survreg(Surv(time, status) ~ x, dist = "lognormal") comparison <- compare_multiple_survreg(fit1, fit2) ## End(Not run)## Not run: fit1 <- survreg(Surv(time, status) ~ x, dist = "weibull") fit2 <- survreg(Surv(time, status) ~ x, dist = "lognormal") comparison <- compare_multiple_survreg(fit1, fit2) ## End(Not run)
Runs forestsearch once per combination of
sg_focus[i] + selection_rule[i] (tuple semantics; the
two vectors are paired element-by-element and must have the same
length). All other arguments are held fixed across runs and passed
through via ....
compare_selection_rules( df.analysis, sg_focus, selection_rule, compute_cis = TRUE, n_splits = 1000L, ci_seed = 1L, plot_xlim = NULL, show_band = TRUE, combo_labels = NULL, verbose = TRUE, ... )compare_selection_rules( df.analysis, sg_focus, selection_rule, compute_cis = TRUE, n_splits = 1000L, ci_seed = 1L, plot_xlim = NULL, show_band = TRUE, combo_labels = NULL, verbose = TRUE, ... )
df.analysis |
Analysis-ready data frame; passed to
|
sg_focus |
Character vector of |
selection_rule |
Character vector of |
compute_cis |
Logical. If |
n_splits |
Integer. Passed to
|
ci_seed |
Integer. Passed to
|
plot_xlim |
Numeric vector of length 2 (or |
show_band |
Logical. Whether to draw the effect-band
shading on each plot. Default |
combo_labels |
Character vector of length |
verbose |
Logical. Print one line per combination during
execution. Default |
... |
Additional arguments passed through to every
|
For each combination the wrapper:
Captures the in-flight console output from forestsearch
(including the pre-consistency Candidate Evaluation
Preview and post-consistency Candidate Evaluation
Summary when show_candidate_summary = TRUE is passed).
Optionally computes the frontier CIs via
compute_frontier_cis.
Builds the Pareto-frontier plot via
plot_pareto_frontier.
The captured stdout is returned per combination so the caller can
cat() it later for side-by-side inspection in a Quarto chunk.
Plot objects are returned individually and (when
patchwork is installed) composed into a single side-by-side
figure. The full fs object for each combination is returned
so downstream diagnostics (pareto_frontier_table,
explain_pareto_selection) can be run without re-fitting.
A list with class "forestsearch_comparison" and
components:
combosdata.frame with columns
sg_focus, selection_rule, label.
fsNamed list of forestsearch result
objects, one per combination. NULL entries mark fits that
errored.
ci_tabNamed list of frontier-CI tables (or all
NULL if compute_cis = FALSE).
plotsNamed list of ggplot objects, one per
combination.
plot_gridA single patchwork object placing
the plots side by side, or NULL if patchwork isn't
installed.
plot_combinedA single ggplot composing all
configurations onto one Pareto plot, with each winner labeled
S1: <combo_label>, S2: <combo_label>, etc.
Returned only when ALL successful configurations share an
identical passing set (see plot_pareto_combined
for the equality criterion); NULL otherwise. See
plot_combined_subsets below for the per-subset
fallback when some – but not all – configurations match.
plot_combined_subsetsNamed list of ggplot
objects, one per equivalence class of configurations sharing
an identical passing set. Only groups of size
are included (a singleton group has no peer to combine
with). Names are derived from the shared sg_focus
when constant within a group (e.g., "effMaxSG"); from
the concatenated combo labels otherwise. When all valid
combos share one passing set this list contains exactly one
element, equal to plot_combined. The list is empty
when no two combos match.
combined_skip_reasonCharacter scalar or
NULL. When plot_combined is NULL
because the equality precondition failed, this captures the
specific reason – size mismatch, definition-set mismatch,
or value drift on a named column – as a single string.
NULL when the combined plot was built or when fewer
than two fits succeeded.
consoleNamed list of character vectors – the captured stdout from each forestsearch run (full output).
diagnosticsNamed list of per-combo slices of the
captured output: each element is a list with character-scalar
fields preview (the CANDIDATE EVALUATION
PREVIEW block), summary (the CANDIDATE
EVALUATION SUMMARY block), and full (the entire
captured stdout, same content as console[[i]]
collapsed to a single string). preview and
summary are NA_character_ when the corresponding
banner is absent (e.g.\ show_candidate_summary was
FALSE, or the run errored before reaching it). This
slice is what a comparison document should display as the
primary diagnostic; full (or console[[i]])
remains available for an expandable / on-demand view of the
complete run output.
errorsNamed list of error messages (or NULL) for combos that failed.
With sg_focus = c("hrMaxSG", "hrMaxSG") and
selection_rule = c("pareto", "both"), two runs are
performed: one with (hrMaxSG, pareto) and one with
(hrMaxSG, both). The length-mismatch case raises an error.
All stdout from each forestsearch() call is captured (not
just the diagnostic tables) via capture.output(). The
PREVIEW / SUMMARY blocks are clearly delineated by banner separators
within the captured text. Use cat(out$console[[i]]) to
replay the output in a Quarto chunk; results = "asis" works
but is not required since the captured text already includes
newlines.
forestsearch,
plot_pareto_frontier,
plot_pareto_combined,
pareto_frontier_table,
explain_pareto_selection,
compute_frontier_cis.
## Not run: out <- compare_selection_rules( df.analysis = actg_df, sg_focus = c("hrMaxSG", "hrMaxSG"), selection_rule = c("pareto", "both"), # All other forestsearch args: confounders.name = confounders.name, outcome.name = adverse_outcome, treat.name = treat.name, id.name = id.name, outcome_type = "continuous", effect_measure = "MD", adverse_outcome = TRUE, hr.threshold = 10, hr.consistency = 5, pconsistency.threshold = 0.90, show_candidate_summary = TRUE, details = TRUE ) # Inspect captured console output for combo 1 cat(out$console[[1]]) # Side-by-side plot print(out$plot_grid) # Use fs object for downstream diagnostics pareto_frontier_table(out$fs[[1]], ci_table = out$ci_tab[[1]]) explain_pareto_selection(out$fs[[2]], ci_table = out$ci_tab[[2]]) ## End(Not run)## Not run: out <- compare_selection_rules( df.analysis = actg_df, sg_focus = c("hrMaxSG", "hrMaxSG"), selection_rule = c("pareto", "both"), # All other forestsearch args: confounders.name = confounders.name, outcome.name = adverse_outcome, treat.name = treat.name, id.name = id.name, outcome_type = "continuous", effect_measure = "MD", adverse_outcome = TRUE, hr.threshold = 10, hr.consistency = 5, pconsistency.threshold = 0.90, show_candidate_summary = TRUE, details = TRUE ) # Inspect captured console output for combo 1 cat(out$console[[1]]) # Side-by-side plot print(out$plot_grid) # Use fs object for downstream diagnostics pareto_frontier_table(out$fs[[1]], ci_table = out$ci_tab[[1]]) explain_pareto_selection(out$fs[[2]], ci_table = out$ci_tab[[2]]) ## End(Not run)
Builds the per-subgroup comparison frame between two
run_subgroup_sims() results (or RDS payloads loaded from the
vignettes – any list carrying sim_hrs / sim_ubs / sim_ns
matrices with identical column names). For each input the per-design
statistics replicate the vignettes' Section 6.6.1 definitions
exactly: tail probabilities are percentages denominated over
converged fits (na.rm = TRUE), medians are unconditional, N is
the across-trial mean subgroup size, and structurally empty
subgroups (all-NA columns) are mapped from NaN to NA.
compare_subgroup_sims( x, y, expect_designs = NULL, suffixes = c("_r", "_f"), est_thresholds = NULL, ub_thresholds = NULL )compare_subgroup_sims( x, y, expect_designs = NULL, suffixes = c("_r", "_f"), est_thresholds = NULL, ub_thresholds = NULL )
x, y
|
The two studies to compare, in display order (the memo passes random-X then fixed-X). |
expect_designs |
Optional length-2 character: required |
suffixes |
Length-2 character appended to each statistic's
column for |
est_thresholds |
Length-2 numeric |
ub_thresholds |
Length-2 numeric for the |
Column names are structural and retained across outcome types
(hr05, hr1, ub2, ub3, mHR, mUB), exactly as sim_hrs
serves generic duty on GLM results: for subgroup_glm() fits they
hold estimate-scale statistics at the resolved thresholds. Threshold
resolution: explicit arguments win; otherwise the inputs' effect
metadata supplies them; otherwise the HR legacy c(0.5, 1.0) /
c(2, 3). An NA threshold yields an all-NA column. The two
inputs must carry compatible metadata (both none, or the same
effect measure) – comparing an MD study against an HR study is a
hard error, raised before the panel-alignment guard so the
categorical incompatibility is reported first.
Two validations guard the row-wise alignment (formerly the memo's
Guard 1 and Guard 2): the subgroup panels must be identical, and,
when expect_designs is supplied, each input's design label must
match it exactly.
The medians here use median(), matching the memo; they are equal
to summary.subgroup_sims()'s type-7 quantile(..., 0.50) values
up to floating-point formulation ((a + b) / 2 versus
a + 0.5 * (b - a)), which is why the memo's numbers and the
vignettes' tables have always agreed to the printed digit.
A plain data.frame (one row per subgroup, matching the
memo's static-mode frame type) with columns subgroup, then for
each of N, ub2, ub3, mUB, hr05, hr1, mHR the x and
y columns interleaved. The inputs' n_sims and design values
are attached as attributes "n_sims" and "designs"; when the
inputs carry effect metadata it is attached as attribute
"effect" (informational; attributes are dropped by most
transformations).
run_subgroup_sims(), summary.subgroup_sims(),
subgroup_glm()
## Not run: pl_r <- readRDS("results/extreme_sims_resample_10000_payload.rds") pl_f <- readRDS("results/extreme_sims_fixed_10000_payload.rds") cmp <- compare_subgroup_sims(pl_r, pl_f, expect_designs = c("resample", "fixed")) ## End(Not run)## Not run: pl_r <- readRDS("results/extreme_sims_resample_10000_payload.rds") pl_f <- readRDS("results/extreme_sims_fixed_10000_payload.rds") cmp <- compare_subgroup_sims(pl_r, pl_f, expect_designs = c("resample", "fixed")) ## End(Not run)
Calculates the probability that a true subgroup with given hazard ratio will be detected using the ForestSearch consistency-based criteria.
compute_detection_probability( theta, n_sg, prop_cens = 0.3, hr_threshold = 1.25, hr_consistency = 1, method = c("cubature", "monte_carlo"), n_mc = 100000L, tol = 1e-04, verbose = FALSE )compute_detection_probability( theta, n_sg, prop_cens = 0.3, hr_threshold = 1.25, hr_consistency = 1, method = c("cubature", "monte_carlo"), n_mc = 100000L, tol = 1e-04, verbose = FALSE )
theta |
Numeric. True hazard ratio in the subgroup. Can be a vector for computing detection probability across multiple HR values. |
n_sg |
Integer. Subgroup sample size. |
prop_cens |
Numeric. Proportion censored (0-1). Default: 0.3 |
hr_threshold |
Numeric. HR threshold for detection (e.g., 1.25). This is the threshold that the average HR across splits must exceed. |
hr_consistency |
Numeric. HR consistency threshold (e.g., 1.0). This is the threshold each individual split must exceed. Default: 1.0 |
method |
Character. Integration method: "cubature" (recommended for accuracy) or "monte_carlo" (faster for exploration). Default: "cubature" |
n_mc |
Integer. Number of Monte Carlo samples if method = "monte_carlo". Default: 100000 |
tol |
Numeric. Relative tolerance for cubature integration. Default: 1e-4 |
verbose |
Logical. Print progress for vector inputs. Default: FALSE |
This function computes P(detect | theta) using the asymptotic normal approximation for the log hazard ratio estimator. The detection criterion is based on ForestSearch's split-sample consistency evaluation:
The subgroup HR estimate must exceed hr_threshold on average
Each split-half must individually exceed hr_consistency
The approximation assumes:
Large sample sizes (CLT applies)
Var(log(HR)) ~ 4/d for the full subgroup (d = total subgroup events); each 50/50 split-half (d/2 events) has variance 8/d
Independence between split-halves (conditional on true effect)
If theta is scalar, returns a single probability. If theta is a vector, returns a data.frame with columns: theta, probability.
## Not run: # Single HR value prob <- compute_detection_probability( theta = 1.5, n_sg = 60, prop_cens = 0.2, hr_threshold = 1.25 ) # Vector of HR values for power curve hr_values <- seq(1.0, 2.5, by = 0.1) results <- compute_detection_probability( theta = hr_values, n_sg = 60, prop_cens = 0.2, hr_threshold = 1.25, verbose = TRUE ) # Plot detection probability curve plot(results$theta, results$probability, type = "l", xlab = "True HR", ylab = "P(detect)") ## End(Not run)## Not run: # Single HR value prob <- compute_detection_probability( theta = 1.5, n_sg = 60, prop_cens = 0.2, hr_threshold = 1.25 ) # Vector of HR values for power curve hr_values <- seq(1.0, 2.5, by = 0.1) results <- compute_detection_probability( theta = hr_values, n_sg = 60, prop_cens = 0.2, hr_threshold = 1.25, verbose = TRUE ) # Plot detection probability curve plot(results$theta, results$probability, type = "l", xlab = "True HR", ylab = "P(detect)") ## End(Not run)
Extends the Section 2.1 approximation (Leon et al., 2024, eq. 3) to
arbitrary GLM outcomes via the effective information
. This is the GLM generalization of
compute_detection_probability.
compute_detection_probability_glm( theta, d_eff, c1 = 1.25, c2 = 1, effect_scale = c("ratio", "difference"), method = c("cubature", "monte_carlo"), n_mc = 500000L, seed = 42L, verbose = FALSE )compute_detection_probability_glm( theta, d_eff, c1 = 1.25, c2 = 1, effect_scale = c("ratio", "difference"), method = c("cubature", "monte_carlo"), n_mc = 500000L, seed = 42L, verbose = FALSE )
theta |
Numeric (scalar or vector). True treatment effect in the subgroup on the natural scale:
|
d_eff |
Numeric. Effective information in the subgroup.
Use the |
c1 |
Numeric. Screening threshold on the natural scale. For ratio measures: the ratio (e.g., 1.25). For difference measures: the difference (e.g., 0.2). |
c2 |
Numeric. Consistency threshold on the natural scale. Default: 1.0 for ratio, 0.0 for difference. |
effect_scale |
Character. |
method |
Character. |
n_mc |
Integer. Monte Carlo samples if method = "monte_carlo". |
seed |
Integer. RNG seed for Monte Carlo. |
verbose |
Logical. Progress messages for vector theta. |
The detection criterion (Leon et al., 2024, Section 2.1):
Screening:
Consistency:
with independently.
For ratio-scale effects, ,
.
For difference-scale effects, , .
If theta is scalar, a single probability.
If vector, a data.frame with columns theta, probability.
compute_detection_probability for the
survival-specific version.
# Survival: reproduce Figure 2 d <- d_eff_survival(n_sg = 60, prop_cens = 0.45) compute_detection_probability_glm(theta = 2.0, d_eff = d) # Binary: OR = 2.0, event rate 30% d <- d_eff_binary(n_sg = 100, p_event = 0.30) compute_detection_probability_glm(theta = 2.0, d_eff = d) # Count: IRR = 2.0, 80 total events d <- d_eff_count(total_events = 80) compute_detection_probability_glm(theta = 2.0, d_eff = d) # Continuous: mean difference = 0.5, sigma = 1.5 d <- d_eff_continuous(n_sg = 100, sigma_y = 1.5) compute_detection_probability_glm( theta = 0.5, d_eff = d, c1 = 0.2, c2 = 0.0, effect_scale = "difference" ) # Power curve d <- d_eff_binary(100, 0.30) result <- compute_detection_probability_glm( theta = seq(0.5, 3.0, by = 0.1), d_eff = d ) plot(result$theta, result$probability, type = "l")# Survival: reproduce Figure 2 d <- d_eff_survival(n_sg = 60, prop_cens = 0.45) compute_detection_probability_glm(theta = 2.0, d_eff = d) # Binary: OR = 2.0, event rate 30% d <- d_eff_binary(n_sg = 100, p_event = 0.30) compute_detection_probability_glm(theta = 2.0, d_eff = d) # Count: IRR = 2.0, 80 total events d <- d_eff_count(total_events = 80) compute_detection_probability_glm(theta = 2.0, d_eff = d) # Continuous: mean difference = 0.5, sigma = 1.5 d <- d_eff_continuous(n_sg = 100, sigma_y = 1.5) compute_detection_probability_glm( theta = 0.5, d_eff = d, c1 = 0.2, c2 = 0.0, effect_scale = "difference" ) # Power curve d <- d_eff_binary(100, 0.30) result <- compute_detection_probability_glm( theta = seq(0.5, 3.0, by = 0.1), d_eff = d ) plot(result$theta, result$probability, type = "l")
Calculates Controlled Direct Effect (CDE) hazard ratios from the
super-population potential outcomes (theta_0, theta_1)
and attaches them to the DGM's hazard_ratios list. This enables
automatic CDE detection by build_estimation_table.
compute_dgm_cde(dgm, harm_col = NULL)compute_dgm_cde(dgm, harm_col = NULL)
dgm |
A DGM object (e.g., from |
harm_col |
Character. Name of the subgroup indicator column in
|
The CDE for subgroup S is defined as:
CDE(S) = mean(exp(theta_1[S])) / mean(exp(theta_0[S]))
which is the ratio of average hazard contributions on the natural scale.
This differs from the AHR (exp(mean(loghr_po))) due to Jensen's
inequality. In the notation of Leon et al. (2024), CDE corresponds to
theta-ddagger.
The function detects the subgroup indicator column automatically,
checking for flag.harm, flag_harm, and H in
the super-population data frame.
The DGM object with CDE values added to
dgm$hazard_ratios (CDE, CDE_harm,
CDE_no_harm) and to top-level fields (dgm$CDE,
dgm$cde_H, dgm$cde_Hc).
build_estimation_table, get_dgm_hr
## Not run: dgm <- create_gbsg_dgm(model = "alt", k_inter = 2.0) dgm <- compute_dgm_cde(dgm) dgm$hazard_ratios$CDE_harm # theta-ddagger(H) dgm$hazard_ratios$CDE # theta-ddagger overall ## End(Not run)## Not run: dgm <- create_gbsg_dgm(model = "alt", k_inter = 2.0) dgm <- compute_dgm_cde(dgm) dgm$hazard_ratios$CDE_harm # theta-ddagger(H) dgm$hazard_ratios$CDE # theta-ddagger overall ## End(Not run)
For each candidate subgroup on fs$grp.consistency$out_sg$pareto_frontier,
computes three 95\
compute_frontier_cis( fs, n_splits = 1000L, ci_level = 0.95, seed = NULL, verbose = FALSE )compute_frontier_cis( fs, n_splits = 1000L, ci_level = 0.95, seed = NULL, verbose = FALSE )
fs |
A |
n_splits |
Integer. Number of 50/50 random splits per frontier
member for the Split CI. Default |
ci_level |
Numeric in |
seed |
Integer or |
verbose |
Logical. Default |
Naive CI: full-sample Wald CI from a Cox or GLM model refit on the subgroup's data. This is the textbook CI that ignores the multiple-testing / subgroup-search context. Anti-conservative by construction.
Split CI: a 95\
estimate, with SE estimated by the empirical standard deviation of
the individual half-sample effect estimates produced by
n_splits random 50/50 splits. This is a half-jackknife SE
(Shao1996). Naming: the in column labels marks
this as a resampling-derived approximation, not a model-based CI.
FSBC-mimic CI: a bias-corrected interval inspired by
the bootstrap bias-correction algorithm of Leon2024fs
(eq 7), with the selection-on-bootstrap-data term
zeroed out (the selected subgroup is
treated as fixed across half-jackknife replicates). The
bias-corrected estimate is
where is the mean of the
half-sample estimates; the SE is the same
half-jackknife SE as for the Split CI. See
“Details — FSBC-mimic interpretation” below.
All three CIs are computed post-hoc on the returned forestsearch
object; this function does not modify any internal state.
A data.table keyed by frontier row index (m) with
columns:
mCandidate id (matches pareto_frontier$m).
estimateRefit effect estimate on the natural scale.
naive_lcl, naive_ucl
Naive Wald CI bounds.
split_sdHalf-jackknife SE estimate: empirical SD of
the per-half effect estimates on the SE scale (log for
ratio measures, natural for differences).
split_lcl, split_ucl
Split-derived CI bounds.
fsbc_estimateBias-corrected effect estimate on the
natural scale ( on
the SE scale, then back-transformed).
fsbc_lcl, fsbc_ucl
FSBC-mimic CI bounds.
n_valid_splitsNumber of splits that produced valid per-half estimates.
Rows where the refit fails (e.g., zero events in one arm) have
NA in all CI columns.
For each of n_splits random 50/50 splits we fit the effect model
separately on each half, obtaining per-half estimates
. The half-jackknife
SE estimator is
i.e., the plain empirical SD of the estimates. No
correction is applied: simulation studies confirm that
this estimator's CI achieves nominal coverage in a no-selection
setting (see the package's pressure-test script). Earlier versions
of this function used the average per split; that approach is invalid because
the two halves are complementary and their average is nearly
constant.
The FSBC-mimic CI is a quick approximation to the bootstrap
bias-corrected estimator of Leon2024fs. Substituting the
per-half estimates for bootstrap replicates, and treating
the selected subgroup as fixed (i.e., omitting the
term that captures variability in FS selection
on the bootstrap data), the bias-corrected estimator from paper
eq (7) collapses to
The SE is the same half-jackknife SE as for the Split CI.
The FSBC-mimic CI is not a full FSBC CI: it omits the
selection-on-bootstrap-data variance. Use this column as a quick
bias-corrected diagnostic, not for inference. For full FSBC CIs use
forestsearch_bootstrap_dofuture.
pareto_frontier_table,
frontier_member_flags, plot_pareto_frontier.
Aggregates scores by leaf node and calculates treatment effect differences
compute_node_metrics(data, dr.scores, tree, X, n.min)compute_node_metrics(data, dr.scores, tree, X, n.min)
data |
Data frame. Original data |
dr.scores |
Matrix. Doubly robust scores |
tree |
Policy tree object |
X |
Matrix. Covariate matrix |
n.min |
Integer. Minimum subgroup size |
Data frame with node metrics
## Not run: # compute_node_metrics() is called internally by grf.subg.harm.survival(). # See grf.subg.harm.survival() for the standard entry point. ## End(Not run)## Not run: # compute_node_metrics() is called internally by grf.subg.harm.survival(). # See grf.subg.harm.survival() for the standard entry point. ## End(Not run)
Computes normal-approximation confidence intervals
using the
sandwich (or Cox model-based) variance returned by dina_fit().
## S3 method for class 'dina' confint(object, parm, level = 0.95, ...)## S3 method for class 'dina' confint(object, parm, level = 0.95, ...)
object |
an object of class |
parm |
optional character or integer vector selecting which coefficients to return intervals for; defaults to all. |
level |
confidence level (default |
... |
unused. |
a matrix with two columns giving lower and upper limits, named
in standard confint() style (e.g. "2.5 %", "97.5 %").
Approximates the forestsearch consistency rate for a candidate subgroup
without repeated sample splits and refits. The subgroup is fit once; the
random-split pair of half-sample treatment effects is represented as
beta_hat +/- D, a multiplier sum of the per-subject treatment dfbeta
contributions (see the file header). Handles the Cox (survival) path and the
GLM paths (binary OR/RR/RD, continuous MD, count IRR).
consistency_resample( df, hr.consistency = 1, consistency_threshold = NULL, comparison_threshold = NULL, outcome_type = c("survival", "binary", "continuous", "count"), effect_measure = NULL, method = c("both", "closed", "mc"), multiplier = c("rademacher", "normal", "poisson"), draws = 1000L, tte.name = "Y", event.name = "Event", treat.name = "Treat", outcome.name = "Y", offset.name = NULL, cox_init = 0, adjust_covariates = NULL, adverse_outcome = TRUE, seed = NULL )consistency_resample( df, hr.consistency = 1, consistency_threshold = NULL, comparison_threshold = NULL, outcome_type = c("survival", "binary", "continuous", "count"), effect_measure = NULL, method = c("both", "closed", "mc"), multiplier = c("rademacher", "normal", "poisson"), draws = 1000L, tte.name = "Y", event.name = "Event", treat.name = "Treat", outcome.name = "Y", offset.name = NULL, cox_init = 0, adjust_covariates = NULL, adverse_outcome = TRUE, seed = NULL )
df |
data.frame/data.table with the relevant outcome columns. |
hr.consistency |
Numeric; survival HR consistency threshold (also the
default GLM threshold when |
consistency_threshold |
Numeric or |
comparison_threshold |
Numeric or |
outcome_type |
Character; |
effect_measure |
Character or |
method |
Character; |
multiplier |
Character; |
draws |
Integer; multiplier draws for the Monte Carlo path. |
tte.name, event.name, treat.name
|
Character; survival/treatment column names. |
outcome.name, offset.name
|
Character; GLM outcome and (rate) offset column names. |
cox_init |
Numeric; Cox warm-start. |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
adverse_outcome |
Logical; GLM adverse-outcome convention (see
|
seed |
Integer or |
multiplier = "rademacher" (default) reproduces the 50/50 Bernoulli split of
run_single_consistency_split(); "normal" is the Lin (1997) Gaussian
multiplier and "poisson" a centred unit-Poisson (DBP) multiplier.
List with rate_closed, rate_mc, beta_hat, sigma_D, n, d,
delta, measure; all-NA rates (with available summaries) if the fit
fails or the configuration is unsupported.
run_single_consistency_split(), consistency_resample_compare()
## Not run: # survival (Cox) consistency_resample(sub, hr.consistency = 1.0) # binary odds ratio consistency_resample(sub, outcome_type = "binary", effect_measure = "OR", consistency_threshold = 1.0, outcome.name = "Y", treat.name = "Treat") # count incidence-rate ratio consistency_resample(sub, outcome_type = "count", effect_measure = "IRR", consistency_threshold = 1.0, outcome.name = "events", offset.name = "fu_time") ## End(Not run)## Not run: # survival (Cox) consistency_resample(sub, hr.consistency = 1.0) # binary odds ratio consistency_resample(sub, outcome_type = "binary", effect_measure = "OR", consistency_threshold = 1.0, outcome.name = "Y", treat.name = "Treat") # count incidence-rate ratio consistency_resample(sub, outcome_type = "count", effect_measure = "IRR", consistency_threshold = 1.0, outcome.name = "events", offset.name = "fu_time") ## End(Not run)
Calls the package's run_single_consistency_split() R_true times and
compares the Monte Carlo rate to the closed-form and multiplier
approximations from consistency_resample(). Survival path only; for the
GLM paths use the standalone GLM validator.
consistency_resample_compare( df, hr.consistency = 1, R_true = 400L, draws = 1000L, multiplier = "rademacher", tte.name = "Y", event.name = "Event", treat.name = "Treat", cox_init = 0, adjust_covariates = NULL, seed = NULL )consistency_resample_compare( df, hr.consistency = 1, R_true = 400L, draws = 1000L, multiplier = "rademacher", tte.name = "Y", event.name = "Event", treat.name = "Treat", cox_init = 0, adjust_covariates = NULL, seed = NULL )
df |
data.frame/data.table with the relevant outcome columns. |
hr.consistency |
Numeric; survival HR consistency threshold (also the
default GLM threshold when |
R_true |
Integer; number of literal random splits used for the truth. |
draws |
Integer; multiplier draws for the Monte Carlo path. |
multiplier |
Character; |
tte.name, event.name, treat.name
|
Character; survival/treatment column names. |
cox_init |
Numeric; Cox warm-start. |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
seed |
Integer or |
One-row data.frame with n, d, beta_hat, sigma_D,
rate_true, valid_true, rate_closed, rate_mc, err_closed,
err_mc.
consistency_resample(), run_single_consistency_split()
Counts the number of times an ID appears in a bootstrap sample.
count_boot_id(x, dfb)count_boot_id(x, dfb)
x |
ID value. |
dfb |
Data frame of bootstrap sample. |
Integer count of occurrences.
df_boot <- data.frame(id = c(1, 2, 1, 3, 1), id_boot = 1:5) count_boot_id(1, df_boot) # returns 3 count_boot_id(4, df_boot) # returns 0df_boot <- data.frame(id = c(1, 2, 1, 3, 1), id_boot = 1:5) count_boot_id(1, df_boot) # returns 3 count_boot_id(4, df_boot) # returns 0
This wrapper function combines Cox spline fitting with comprehensive visualization of Average Hazard Ratios (AHRs) and Controlled Direct Effects (CDEs) as described in the MRCT subgroups analysis documentation.
cox_ahr_cde_analysis( df, tte_name = "os_time", event_name = "os_event", treat_name = "treat", z_name = "biomarker", loghr_po_name = "loghr_po", theta1_name = "theta_1", theta0_name = "theta_0", spline_df = 3, alpha = 0.2, hr_threshold = 0.7, plot_style = c("combined", "separate", "grid"), plot_select = c("all", "profile_ahr", "ahr_only"), save_plots = FALSE, output_dir = "plots", verbose = TRUE )cox_ahr_cde_analysis( df, tte_name = "os_time", event_name = "os_event", treat_name = "treat", z_name = "biomarker", loghr_po_name = "loghr_po", theta1_name = "theta_1", theta0_name = "theta_0", spline_df = 3, alpha = 0.2, hr_threshold = 0.7, plot_style = c("combined", "separate", "grid"), plot_select = c("all", "profile_ahr", "ahr_only"), save_plots = FALSE, output_dir = "plots", verbose = TRUE )
df |
Data frame containing survival data with potential outcomes. |
tte_name |
Character string specifying time-to-event variable name.
Default: |
event_name |
Character string specifying event indicator variable
name. Default: |
treat_name |
Character string specifying treatment variable name.
Default: |
z_name |
Character string specifying continuous covariate/biomarker
name. Default: |
loghr_po_name |
Character string specifying potential outcome log HR
variable. Default: |
theta1_name |
Optional: variable name for theta_1 (treated potential
outcome). Default: |
theta0_name |
Optional: variable name for theta_0 (control potential
outcome). Default: |
spline_df |
Integer degrees of freedom for spline fitting. Default: 3. |
alpha |
Numeric significance level for confidence intervals. Default: 0.20. |
hr_threshold |
Numeric hazard ratio threshold for subgroup
identification, or |
plot_style |
Character: |
plot_select |
Character controlling which panels to display:
|
save_plots |
Logical whether to save plots to file. Default: FALSE. |
output_dir |
Character directory for saving plots. Default:
|
verbose |
Logical for diagnostic output. Default: TRUE. |
List of class "cox_ahr_cde" containing:
Results from cox_cs_fit function.
AHR calculations for different subgroup definitions.
CDE calculations if theta variables available.
Optimal biomarker cutpoint, or NULL when
hr_threshold is NULL.
Statistics for recommended and questionable
subgroups, or overall-only when hr_threshold is NULL.
List with z_values, loghr_po, and subgroup assignments.
## Not run: # With threshold - full subgroup analysis results <- cox_ahr_cde_analysis( df = df_large, z_name = "z_bm", hr_threshold = 1.25, plot_style = "grid" ) # Without threshold - pure AHR/CDE curves results <- cox_ahr_cde_analysis( df = df_large, z_name = "z_bm", hr_threshold = NULL, plot_style = "grid" ) # Compact two-panel without threshold results <- cox_ahr_cde_analysis( df = df_large, z_name = "z_bm", hr_threshold = NULL, plot_select = "profile_ahr" ) # Single AHR panel only results <- cox_ahr_cde_analysis( df = df_large, z_name = "z_bm", hr_threshold = 1.25, plot_select = "ahr_only" ) ## End(Not run)## Not run: # With threshold - full subgroup analysis results <- cox_ahr_cde_analysis( df = df_large, z_name = "z_bm", hr_threshold = 1.25, plot_style = "grid" ) # Without threshold - pure AHR/CDE curves results <- cox_ahr_cde_analysis( df = df_large, z_name = "z_bm", hr_threshold = NULL, plot_style = "grid" ) # Compact two-panel without threshold results <- cox_ahr_cde_analysis( df = df_large, z_name = "z_bm", hr_threshold = NULL, plot_select = "profile_ahr" ) # Single AHR panel only results <- cox_ahr_cde_analysis( df = df_large, z_name = "z_bm", hr_threshold = 1.25, plot_select = "ahr_only" ) ## End(Not run)
Estimates treatment effects as a function of a continuous covariate using a Cox proportional hazards model with natural cubic splines. The function models treatment-by-covariate interactions to detect effect modification.
cox_cs_fit( df, tte_name = "os_time", event_name = "os_event", treat_name = "treat", strata_name = NULL, z_name = "bm", alpha = 0.2, spline_df = 3, z_max = Inf, z_by = 1, z_window = 0, z_quantile = 0.9, show_plot = TRUE, plot_params = NULL, truebeta_name = NULL, verbose = TRUE )cox_cs_fit( df, tte_name = "os_time", event_name = "os_event", treat_name = "treat", strata_name = NULL, z_name = "bm", alpha = 0.2, spline_df = 3, z_max = Inf, z_by = 1, z_window = 0, z_quantile = 0.9, show_plot = TRUE, plot_params = NULL, truebeta_name = NULL, verbose = TRUE )
df |
Data frame containing survival data |
tte_name |
Character string specifying time-to-event variable name. Default: "os_time" |
event_name |
Character string specifying event indicator variable name (1=event, 0=censored). Default: "os_event" |
treat_name |
Character string specifying treatment variable name (1=treated, 0=control). Default: "treat" |
strata_name |
Character string specifying stratification variable name. If NULL, no stratification is used. Default: NULL |
z_name |
Character string specifying continuous covariate name for effect modification. Default: "bm" |
alpha |
Numeric value for confidence level (two-sided). Default: 0.20 (80% confidence intervals) |
spline_df |
Integer specifying degrees of freedom for natural spline. Default: 3 |
z_max |
Numeric maximum value for z in predictions. Values beyond this are truncated. Default: Inf (no truncation) |
z_by |
Numeric increment for z values in prediction grid. Default: 1 |
z_window |
Numeric half-width for counting observations near each z value. Default: 0.0 (exact matches only) |
z_quantile |
Numeric quantile (0-1) for upper limit of z profile. Default: 0.90 (90th percentile) |
show_plot |
Logical indicating whether to display plot. Default: TRUE |
plot_params |
List of plotting parameters (see Details). Default: NULL |
truebeta_name |
Character string specifying variable containing true log(HR) values for validation/simulation. Default: NULL |
verbose |
Logical indicating whether to print diagnostic information. Default: TRUE |
The function fits:
Where:
A is treatment (0/1)
Z is the continuous effect modifier
f(Z) is modeled with natural splines (main effect)
g(Z) is modeled with natural splines (interaction)
The log hazard ratio is:
The plot_params argument accepts a list with:
xlab: x-axis label
main_title: plot title
ylimit: y-axis limits c(min, max)
y_pad_zero: padding below zero line
y_delta: extra space for count labels
cex_legend: legend text size
cex_count: count text size
show_cox_primary: show standard Cox estimate line
show_null: show null effect line (log(HR)=0)
show_target: show target effect line (e.g., log(0.80))
List containing:
Vector of z values where treatment effect is estimated
Point estimates of log(HR) at each z value
Lower confidence bound
Upper confidence bound
Standard errors of log(HR) estimates
Number of observations near each z value
Log(HR) from standard Cox model (no interaction)
The fitted coxph model object
The natural spline basis object
## Not run: # Simulate data set.seed(123) df <- data.frame( os_time = rexp(500, 0.01), os_event = rbinom(500, 1, 0.7), treat = rbinom(500, 1, 0.5), bm = rnorm(500, 50, 10) ) # Fit model result <- cox_cs_fit(df, z_name = "bm", alpha = 0.20) # Custom plotting result <- cox_cs_fit( df, z_name = "bm", plot_params = list( xlab = "Biomarker Level", main_title = "Treatment Effect by Biomarker", cex_legend = 1.2 ) ) ## End(Not run)## Not run: # Simulate data set.seed(123) df <- data.frame( os_time = rexp(500, 0.01), os_event = rbinom(500, 1, 0.7), treat = rbinom(500, 1, 0.5), bm = rnorm(500, 50, 10) ) # Fit model result <- cox_cs_fit(df, z_name = "bm", alpha = 0.20) # Custom plotting result <- cox_cs_fit( df, z_name = "bm", plot_params = list( xlab = "Biomarker Level", main_title = "Treatment Effect by Biomarker", cex_legend = 1.2 ) ) ## End(Not run)
Called in analyze_subgroup() <– SG_tab_estimates
cox_summary( Y, E, Treat, Strata = NULL, use_strata = !is.null(Strata), return_format = c("formatted", "numeric") )cox_summary( Y, E, Treat, Strata = NULL, use_strata = !is.null(Strata), return_format = c("formatted", "numeric") )
Y |
Numeric vector of outcome. |
E |
Numeric vector of event indicators. |
Treat |
Numeric vector of treatment indicators. |
Strata |
Vector of strata (optional). |
use_strata |
Logical. Whether to use strata in the model (default: TRUE if Strata provided). |
return_format |
Character. "formatted" (default) or "numeric" for downstream use. |
Calculates hazard ratio and confidence interval for a subgroup using Cox regression. Optimized version with reduced overhead and better error handling.
Character string with formatted HR and CI (or numeric vector if return_format="numeric").
## Not run: library(survival) cox_summary( Y = gbsg$rfstime / 30.4375, E = gbsg$status, Treat = gbsg$hormon ) ## End(Not run)## Not run: library(survival) cox_summary( Y = gbsg$rfstime / 30.4375, E = gbsg$status, Treat = gbsg$hormon ) ## End(Not run)
Wrapper function to create a data generating mechanism (DGM) for MRCT
simulation scenarios using generate_aft_dgm_flex.
create_dgm_for_mrct( df_case, model_type = c("alt", "null"), log_hrs = NULL, confounder_var = NULL, confounder_effect = NULL, include_regA = TRUE, verbose = FALSE )create_dgm_for_mrct( df_case, model_type = c("alt", "null"), log_hrs = NULL, confounder_var = NULL, confounder_effect = NULL, include_regA = TRUE, verbose = FALSE )
df_case |
Data frame containing case study data |
model_type |
Character. Either "alt" (alternative hypothesis with heterogeneous treatment effects) or "null" (uniform treatment effect) |
log_hrs |
Numeric vector. Log hazard ratios for spline specification. If NULL, defaults are used based on model_type |
confounder_var |
Character. Name of a confounder variable to include with a forced prognostic effect. Default: NULL (no forced effect) |
confounder_effect |
Numeric. Log hazard ratio for confounder_var effect. Only used if confounder_var is specified |
include_regA |
Logical. Include regA as a factor in the model. Default: TRUE |
verbose |
Logical. Print detailed output. Default: FALSE |
Alternative hypothesis: Treatment effect varies by biomarker level (heterogeneous treatment effect). Default log_hrs create HR ranging from 2.0 (harm) to 0.5 (benefit) across biomarker range
Null hypothesis: Uniform treatment effect regardless of biomarker level. Default log_hrs = log(0.7) uniformly
By default, NO prognostic confounder effect is forced. The confounder_var and confounder_effect parameters allow optionally specifying ANY baseline covariate to have a fixed prognostic effect in the outcome model.
The regA variable (region indicator) is included as a factor by default but without a forced effect - its coefficient is estimated from data.
An object of class "aft_dgm_flex" for use with
simulate_from_dgm and mrct_region_sims
generate_aft_dgm_flex for underlying DGM creation
mrct_region_sims for running simulations with the DGM
## Not run: # Prepare data df_case <- read.csv("data/dfsynthetic.csv") df_case$regA <- df_case$region_asia # Alternative hypothesis (heterogeneous treatment effect) dgm_alt <- create_dgm_for_mrct( df_case = df_case, model_type = "alt", log_hrs = log(c(3, 1.25, 0.50)), verbose = TRUE ) # Null hypothesis (uniform effect) dgm_null <- create_dgm_for_mrct( df_case = df_case, model_type = "null", verbose = TRUE ) # With forced confounder effect dgm_conf <- create_dgm_for_mrct( df_case = df_case, model_type = "alt", confounder_var = "prior_treat", confounder_effect = log(1.5), verbose = TRUE ) ## End(Not run)## Not run: # Prepare data df_case <- read.csv("data/dfsynthetic.csv") df_case$regA <- df_case$region_asia # Alternative hypothesis (heterogeneous treatment effect) dgm_alt <- create_dgm_for_mrct( df_case = df_case, model_type = "alt", log_hrs = log(c(3, 1.25, 0.50)), verbose = TRUE ) # Null hypothesis (uniform effect) dgm_null <- create_dgm_for_mrct( df_case = df_case, model_type = "null", verbose = TRUE ) # With forced confounder effect dgm_conf <- create_dgm_for_mrct( df_case = df_case, model_type = "alt", confounder_var = "prior_treat", confounder_effect = log(1.5), verbose = TRUE ) ## End(Not run)
Creates a forestploter theme with parameters that control overall plot sizing and appearance. This is the primary way to control how large the forest plot renders.
create_forest_theme( base_size = 10, scale = 1, row_padding = NULL, ci_pch = 15, ci_lwd = NULL, ci_Theight = NULL, ci_col = "black", header_fontsize = NULL, body_fontsize = NULL, footnote_fontsize = NULL, footnote_col = "darkcyan", title_fontsize = NULL, cv_fontsize = NULL, cv_col = "gray30", refline_lwd = NULL, refline_lty = "dashed", refline_col = "gray30", vertline_lwd = NULL, vertline_lty = "dashed", vertline_col = "gray20", arrow_type = "closed", arrow_col = "black", summary_fill = "black", summary_col = "black" )create_forest_theme( base_size = 10, scale = 1, row_padding = NULL, ci_pch = 15, ci_lwd = NULL, ci_Theight = NULL, ci_col = "black", header_fontsize = NULL, body_fontsize = NULL, footnote_fontsize = NULL, footnote_col = "darkcyan", title_fontsize = NULL, cv_fontsize = NULL, cv_col = "gray30", refline_lwd = NULL, refline_lty = "dashed", refline_col = "gray30", vertline_lwd = NULL, vertline_lty = "dashed", vertline_col = "gray20", arrow_type = "closed", arrow_col = "black", summary_fill = "black", summary_col = "black" )
base_size |
Numeric. Base font size in points. This is the primary scaling parameter - increasing it will proportionally scale all fonts, row padding, and line widths. Default: 10. |
scale |
Numeric. Additional scaling multiplier applied on top of base_size. Use for quick overall scaling. Default: 1.0. |
row_padding |
Numeric vector of length 2. Padding around row content in mm as c(vertical, horizontal). If NULL, auto-calculated from base_size. Default: NULL. |
ci_pch |
Integer. Point character for CI. 15=square, 16=circle, 18=diamond. Default: 15. |
ci_lwd |
Numeric. Line width for CI lines. If NULL, auto-calculated from base_size. Default: NULL. |
ci_Theight |
Numeric. Height of T-bar ends on CI. If NULL, auto-calculated from base_size. Default: NULL. |
ci_col |
Character. Color for CI lines and points. Default: "black". |
header_fontsize |
Numeric. Font size for column headers. If NULL, auto-calculated as base_size * scale + 1. Default: NULL. |
body_fontsize |
Numeric. Font size for body text. If NULL, auto-calculated as base_size * scale. Default: NULL. |
footnote_fontsize |
Numeric. Font size for footnotes. If NULL, auto-calculated as base_size * scale - 1. Default: NULL. |
footnote_col |
Character. Color for footnote text. Default: "darkcyan". |
title_fontsize |
Numeric. Font size for title. If NULL, auto-calculated as base_size * scale + 4. Default: NULL. |
cv_fontsize |
Numeric. Font size for CV annotation text. If NULL, auto-calculated as base_size * scale. Default: NULL. |
cv_col |
Character. Color for CV annotation text. Default: "gray30". |
refline_lwd |
Numeric. Reference line width. If NULL, auto-calculated. Default: NULL. |
refline_lty |
Character. Reference line type. Default: "dashed". |
refline_col |
Character. Reference line color. Default: "gray30". |
vertline_lwd |
Numeric. Vertical line width. If NULL, auto-calculated. Default: NULL. |
vertline_lty |
Character. Vertical line type. Default: "dashed". |
vertline_col |
Character. Vertical line color. Default: "gray20". |
arrow_type |
Character. Arrow type: "open" or "closed". Default: "closed". |
arrow_col |
Character. Arrow color. Default: "black". |
summary_fill |
Character. Fill color for summary diamonds. Default: "black". |
summary_col |
Character. Border color for summary diamonds. Default: "black". |
The base_size parameter is the primary way to control plot size.
When you change base_size, the following are automatically scaled:
All font sizes (body, header, footnote, CV, title)
Row padding (vertical and horizontal)
CI line width and T-bar height
Reference and vertical line widths
The scaling formula uses base_size = 10 as the reference point:
base_size = 10: Default sizing
base_size = 12: 20% larger
base_size = 14: 40% larger
base_size = 16: 60% larger
You can override any individual parameter by specifying it explicitly.
The theme does NOT set row background colors - those are determined
automatically by plot_subgroup_results_forestplot() based on
row types (ITT, reference, posthoc, etc.).
A list of class "fs_forest_theme" containing all theme parameters.
plot_subgroup_results_forestplot, render_forestplot
## Not run: # Simple: just increase base_size for larger plot large_theme <- create_forest_theme(base_size = 14) # Or use scale for quick adjustment large_theme <- create_forest_theme(base_size = 10, scale = 1.4) # Fine-tune specific elements custom_theme <- create_forest_theme( base_size = 14, cv_fontsize = 12, # Override auto-calculated CV font size ci_lwd = 2.5 # Override auto-calculated CI line width ) # Use with plot_subgroup_results_forestplot result <- plot_subgroup_results_forestplot( fs_results = list(fs.est = fs, fs_bc = fs_bc), df_analysis = df.analysis, outcome.name = "time", event.name = "status", treat.name = "treatment", theme = large_theme ) render_forestplot(result) ## End(Not run)## Not run: # Simple: just increase base_size for larger plot large_theme <- create_forest_theme(base_size = 14) # Or use scale for quick adjustment large_theme <- create_forest_theme(base_size = 10, scale = 1.4) # Fine-tune specific elements custom_theme <- create_forest_theme( base_size = 14, cv_fontsize = 12, # Override auto-calculated CV font size ci_lwd = 2.5 # Override auto-calculated CI line width ) # Use with plot_subgroup_results_forestplot result <- plot_subgroup_results_forestplot( fs_results = list(fs.est = fs, fs_bc = fs_bc), df_analysis = df.analysis, outcome.name = "time", event.name = "status", treat.name = "treatment", theme = large_theme ) render_forestplot(result) ## End(Not run)
Creates a data generating mechanism (DGM) for survival simulations based on the German Breast Cancer Study Group (GBSG) dataset. Supports heterogeneous treatment effects via treatment-subgroup interactions.
create_gbsg_dgm( model = c("alt", "null"), k_treat = 1, k_inter = 1, k_z3 = 1, z1_quantile = 0.25, n_super = DEFAULT_N_SUPER, cens_type = c("weibull", "uniform"), use_rand_params = FALSE, seed = SEED_BASE, verbose = FALSE )create_gbsg_dgm( model = c("alt", "null"), k_treat = 1, k_inter = 1, k_z3 = 1, z1_quantile = 0.25, n_super = DEFAULT_N_SUPER, cens_type = c("weibull", "uniform"), use_rand_params = FALSE, seed = SEED_BASE, verbose = FALSE )
model |
Character. Either "alt" for alternative hypothesis with heterogeneous treatment effects, or "null" for uniform treatment effect. Default: "alt" |
k_treat |
Numeric. Treatment effect multiplier applied to the treatment coefficient from the fitted AFT model. Values > 1 strengthen the treatment effect. Default: 1 |
k_inter |
Numeric. Interaction effect multiplier for the treatment-subgroup interaction (z1 * z3). Only used when model = "alt". Higher values create more heterogeneity between HR(H) and HR(Hc). Default: 1 |
k_z3 |
Numeric. Effect multiplier for the z3 (menopausal status) coefficient. Default: 1 |
z1_quantile |
Numeric. Quantile threshold for z1 (estrogen receptor). Observations with ER <= quantile are coded as z1 = 1. Default: 0.25 |
n_super |
Integer. Size of super-population for empirical HR estimation. Default: 5000 |
cens_type |
Character. Censoring distribution type: "weibull" or "uniform". Default: "weibull" |
use_rand_params |
Logical. If TRUE, modifies confounder coefficients using estimates from randomized subset (meno == 0). Default: FALSE |
seed |
Integer. Random seed for super-population generation. Default: 8316951 |
verbose |
Logical. Print diagnostic information. Default: FALSE |
This version is aligned with generate_aft_dgm_flex() and
calculate_hazard_ratios() methodology, computing individual-level
potential outcomes and average hazard ratios (AHR).
The harm subgroup H is defined as: z1 = 1 AND z3 = 1, where:
z1: Low estrogen receptor (ER <= 25th percentile by default)
z3: Premenopausal status (meno == 0)
The AFT model uses covariates: treat, z1, z2, z3, z4, z5, and (for "alt") the interaction zh = treat * z1 * z3.
The k_inter parameter modifies the zh coefficient in the AFT model:
gamma[zh] <- k_inter * gamma[zh]
This affects the hazard ratio for the harm subgroup:
HR(H) = exp(-gamma[treat]/sigma - gamma[zh]/sigma)
HR(Hc) = exp(-gamma[treat]/sigma)
When k_inter = 0, HR(H) = HR(Hc) (no heterogeneity).
This function now computes:
theta_0: Log-hazard contribution under control
theta_1: Log-hazard contribution under treatment
loghr_po: Individual causal log hazard ratio (theta_1 - theta_0)
AHR metrics: exp(mean(loghr_po)) for overall and subgroups
A list of class "gbsg_dgm" containing:
Data frame with randomized super-population including potential outcomes (theta_0, theta_1, loghr_po)
Empirical hazard ratio in harm subgroup (Cox-based)
Empirical hazard ratio in complement subgroup (Cox-based)
Overall causal (ITT) hazard ratio (Cox-based)
Overall average hazard ratio (from loghr_po)
Average hazard ratio in harm subgroup
Average hazard ratio in complement subgroup
List matching generate_aft_dgm_flex output format
List with AFT model parameters (mu, sigma, gamma, etc.)
List with censoring model parameters
List with subgroup definitions and true factor names
Character vector of analysis variable names
Character indicating "alt" or "null"
simulate_from_gbsg_dgm for generating data from the DGM
calibrate_k_inter for finding k_inter to achieve target HR
## Not run: # Alternative hypothesis with default parameters dgm_alt <- create_gbsg_dgm(model = "alt", verbose = TRUE) # Null hypothesis dgm_null <- create_gbsg_dgm(model = "null", verbose = TRUE) # Custom subgroup HR via k_inter dgm_custom <- create_gbsg_dgm( model = "alt", k_treat = 1.2, k_inter = 2.0, verbose = TRUE ) # Access AHR metrics (aligned with generate_aft_dgm_flex) dgm_alt$hazard_ratios$AHR_harm dgm_alt$hazard_ratios$AHR_no_harm ## End(Not run)## Not run: # Alternative hypothesis with default parameters dgm_alt <- create_gbsg_dgm(model = "alt", verbose = TRUE) # Null hypothesis dgm_null <- create_gbsg_dgm(model = "null", verbose = TRUE) # Custom subgroup HR via k_inter dgm_custom <- create_gbsg_dgm( model = "alt", k_treat = 1.2, k_inter = 2.0, verbose = TRUE ) # Access AHR metrics (aligned with generate_aft_dgm_flex) dgm_alt$hazard_ratios$AHR_harm dgm_alt$hazard_ratios$AHR_no_harm ## End(Not run)
Fits a GLM within the specified subgroup and returns a one-row data frame containing the effect estimate, confidence interval, sample size, and event count. Supports binary, continuous, and count (with offset) outcomes.
create_glm_row( df, outcome.name, treat.name = "treat", effect_measure = "log_OR", offset.name = NULL, overdispersion = "none", conf.level = 0.95, min_arm_n = 5L, min_arm_events = 3L, verbose = FALSE )create_glm_row( df, outcome.name, treat.name = "treat", effect_measure = "log_OR", offset.name = NULL, overdispersion = "none", conf.level = 0.95, min_arm_n = 5L, min_arm_events = 3L, verbose = FALSE )
df |
Data frame for the subgroup (already subset). |
outcome.name |
Character. Name of the outcome column. |
treat.name |
Character. Name of the treatment indicator. |
effect_measure |
Character. Effect measure; see
|
offset.name |
Character or |
overdispersion |
Character. One of |
conf.level |
Numeric. Confidence level. Default: |
min_arm_n |
Integer. Minimum observations per arm; returns |
min_arm_events |
Integer. For binary/count outcomes, minimum events
per arm; returns |
verbose |
Logical. Default: |
A one-row data frame with columns:
N, n_treat, n_control,
events_treat, events_control (for binary/count),
est, lower, upper, se, effect_measure.
This file contains helper functions used by grf.subg.harm.survival() to improve readability and modularity. Create GRF configuration object
create_grf_config( frac.tau, n.min, dmin.grf, RCT, sg.criterion, maxdepth, seedit )create_grf_config( frac.tau, n.min, dmin.grf, RCT, sg.criterion, maxdepth, seedit )
frac.tau |
Numeric. Fraction of tau for GRF horizon |
n.min |
Integer. Minimum subgroup size |
dmin.grf |
Numeric. Minimum difference in subgroup mean |
RCT |
Logical. Is the data from a randomized controlled trial? |
sg.criterion |
Character. Subgroup selection criterion |
maxdepth |
Integer. Maximum tree depth |
seedit |
Integer. Random seed |
Creates a configuration object to organize GRF parameters
List with configuration parameters
cfg <- create_grf_config(frac.tau = 0.6, n.min = 60, dmin.grf = 6, RCT = TRUE, sg.criterion = "mDiff", maxdepth = 2, seedit = 42L) str(cfg)cfg <- create_grf_config(frac.tau = 0.6, n.min = 60, dmin.grf = 6, RCT = TRUE, sg.criterion = "mDiff", maxdepth = 2, seedit = 42L) str(cfg)
Generates a formatted summary table comparing baseline characteristics between treatment arms. Supports continuous, categorical, and binary variables with p-values, standardized mean differences (SMD), and missing data summaries.
create_summary_table( data, treat_var = "treat", vars_continuous = NULL, vars_categorical = NULL, vars_binary = NULL, var_labels = NULL, digits = 1, show_pvalue = TRUE, show_smd = TRUE, show_missing = TRUE, table_title = "Baseline Characteristics by Treatment Arm", table_subtitle = NULL, source_note = NULL, font_size = 12, header_font_size = 14, footnote_font_size = 10, use_alternating_rows = TRUE, stripe_color = "#f9f9f9", indent_size = 20, highlight_pval = 0.05, highlight_smd = 0.2, highlight_color = "#fff3cd", compact_mode = FALSE, column_width_var = 200, column_width_stats = 120, show_column_borders = FALSE, custom_css = NULL )create_summary_table( data, treat_var = "treat", vars_continuous = NULL, vars_categorical = NULL, vars_binary = NULL, var_labels = NULL, digits = 1, show_pvalue = TRUE, show_smd = TRUE, show_missing = TRUE, table_title = "Baseline Characteristics by Treatment Arm", table_subtitle = NULL, source_note = NULL, font_size = 12, header_font_size = 14, footnote_font_size = 10, use_alternating_rows = TRUE, stripe_color = "#f9f9f9", indent_size = 20, highlight_pval = 0.05, highlight_smd = 0.2, highlight_color = "#fff3cd", compact_mode = FALSE, column_width_var = 200, column_width_stats = 120, show_column_borders = FALSE, custom_css = NULL )
data |
Data frame containing the analysis data |
treat_var |
Character. Name of treatment variable (must have 2 levels) |
vars_continuous |
Character vector. Names of continuous variables |
vars_categorical |
Character vector. Names of categorical variables |
vars_binary |
Character vector. Names of binary (0/1) variables |
var_labels |
Named list. Custom labels for variables (optional) |
digits |
Integer. Number of decimal places for continuous variables |
show_pvalue |
Logical. Include p-values column |
show_smd |
Logical. Include SMD (effect size) column |
show_missing |
Logical. Include missing data rows |
table_title |
Character. Main title for the table |
table_subtitle |
Character. Subtitle for the table (optional) |
source_note |
Character. Source note at bottom (optional) |
font_size |
Numeric. Base font size in pixels (default: 12) |
header_font_size |
Numeric. Header font size in pixels (default: 14) |
footnote_font_size |
Numeric. Footnote font size in pixels (default: 10) |
use_alternating_rows |
Logical. Apply zebra striping (default: TRUE) |
stripe_color |
Character. Color for alternating rows (default: "#f9f9f9") |
indent_size |
Numeric. Indentation for sub-levels in pixels (default: 20) |
highlight_pval |
Numeric. Highlight p-values below this threshold (default: 0.05) |
highlight_smd |
Numeric. Highlight SMD values above this threshold (default: 0.2) |
highlight_color |
Character. Color for highlighting (default: "#fff3cd") |
compact_mode |
Logical. Reduce spacing for compact display (default: FALSE) |
column_width_var |
Numeric. Width for Variable column in pixels (default: 200) |
column_width_stats |
Numeric. Width for stat columns in pixels (default: 120) |
show_column_borders |
Logical. Show vertical column borders (default: FALSE) |
custom_css |
Character. Additional custom CSS styling (optional) |
Binary variables specified via vars_binary display a single row
showing the count and proportion for the "1" level. Categorical variables
specified via vars_categorical that happen to be binary-coded (i.e.,
have exactly two levels: 0 and 1) are automatically detected and displayed
in the same compact single-row format, showing only the "1" proportion.
A gt table object (or data frame if gt not available)
## Not run: # Basic usage create_summary_table( data = trial_data, treat_var = "treatment", vars_continuous = c("age", "bmi"), vars_categorical = c("sex", "stage") ) # Customized appearance create_summary_table( data = trial_data, treat_var = "treatment", vars_continuous = c("age", "bmi"), vars_categorical = c("sex", "stage"), font_size = 11, header_font_size = 13, use_alternating_rows = TRUE, highlight_pval = 0.05, compact_mode = TRUE ) ## End(Not run)## Not run: # Basic usage create_summary_table( data = trial_data, treat_var = "treatment", vars_continuous = c("age", "bmi"), vars_categorical = c("sex", "stage") ) # Customized appearance create_summary_table( data = trial_data, treat_var = "treatment", vars_continuous = c("age", "bmi"), vars_categorical = c("sex", "stage"), font_size = 11, header_font_size = 13, use_alternating_rows = TRUE, highlight_pval = 0.05, compact_mode = TRUE ) ## End(Not run)
Creates a comparison table from multiple cross-validation runs with different configurations.
cv_compare_results( cv_list, metrics = c("all", "finding", "agreement"), show_percentages = TRUE, digits = 1, use_gt = TRUE )cv_compare_results( cv_list, metrics = c("all", "finding", "agreement"), show_percentages = TRUE, digits = 1, use_gt = TRUE )
cv_list |
Named list of cv_result objects from |
metrics |
Character vector. Which metrics to include. Options: "finding", "agreement", "all". Default: "all". |
show_percentages |
Logical. Display as percentages. Default: TRUE. |
digits |
Integer. Decimal places. Default: 1. |
use_gt |
Logical. Return gt table if TRUE. Default: TRUE. |
A gt table or data.frame comparing CV results across configurations.
## Not run: # Compare CV results from different configurations cv_comparison <- cv_compare_results( cv_list = list( "maxk=1" = cv_maxk1, "maxk=2" = cv_maxk2, "maxk=3" = cv_maxk3 ) ) ## End(Not run)## Not run: # Compare CV results from different configurations cv_comparison <- cv_compare_results( cv_list = list( "maxk=1" = cv_maxk1, "maxk=2" = cv_maxk2, "maxk=3" = cv_maxk3 ) ) ## End(Not run)
Formats the find_summary and sens_summary outputs from
forestsearch_tenfold or forestsearch_Kfold
into publication-ready gt tables.
cv_metrics_tables( cv_result, sg_definition = NULL, title = "Cross-Validation Metrics", show_percentages = TRUE, digits = 1, include_raw = FALSE, table_style = c("combined", "separate", "minimal"), use_gt = TRUE )cv_metrics_tables( cv_result, sg_definition = NULL, title = "Cross-Validation Metrics", show_percentages = TRUE, digits = 1, include_raw = FALSE, table_style = c("combined", "separate", "minimal"), use_gt = TRUE )
cv_result |
List. Result from |
sg_definition |
Character vector. Subgroup factor definitions for
labeling (optional). If NULL, extracted from |
title |
Character. Main title for combined table. Default: "Cross-Validation Metrics". |
show_percentages |
Logical. Display metrics as percentages (0-100) instead of proportions (0-1). Default: TRUE. |
digits |
Integer. Decimal places for formatting. Default: 1. |
include_raw |
Logical. Include raw matrices ( |
table_style |
Character. One of "combined", "separate", or "minimal".
Default: "combined". |
use_gt |
Logical. Return gt table(s) if TRUE, data.frame(s) if FALSE. Default: TRUE. |
Depending on table_style:
"combined": A single gt table (or data.frame)
"separate": A list with agreement_table and finding_table
"minimal": A single-row gt table (or data.frame)
If include_raw = TRUE, also includes sens_out and find_out
matrices in the returned list.
cv_summary_tables for formatting forestsearch_KfoldOut(outall=TRUE) results
## Not run: # After running forestsearch_tenfold tenfold_results <- forestsearch_tenfold( fs.est = fs_result, sims = 100, Kfolds = 10 ) # Create combined metrics table cv_tables <- cv_metrics_tables(tenfold_results) cv_tables # Create separate tables cv_tables <- cv_metrics_tables(tenfold_results, table_style = "separate") cv_tables$agreement_table cv_tables$finding_table # Minimal one-row summary cv_metrics_tables(tenfold_results, table_style = "minimal") ## End(Not run)## Not run: # After running forestsearch_tenfold tenfold_results <- forestsearch_tenfold( fs.est = fs_result, sims = 100, Kfolds = 10 ) # Create combined metrics table cv_tables <- cv_metrics_tables(tenfold_results) cv_tables # Create separate tables cv_tables <- cv_metrics_tables(tenfold_results, table_style = "separate") cv_tables$agreement_table cv_tables$finding_table # Minimal one-row summary cv_metrics_tables(tenfold_results, table_style = "minimal") ## End(Not run)
Summarizes the match between cross-validation subgroups and analysis subgroups.
CV_sgs(sg1, sg2, confs, sg_analysis)CV_sgs(sg1, sg2, confs, sg_analysis)
sg1 |
Character vector. Subgroup 1 labels for each fold. |
sg2 |
Character vector. Subgroup 2 labels for each fold. |
confs |
Character vector. Confounder names. |
sg_analysis |
Character vector. Subgroup analysis labels. |
List with indicators for any match, exact match, one match, and covariate-specific matches.
## Not run: # CV_sgs() is called internally by forestsearch_KfoldOut(). # See forestsearch_Kfold() and forestsearch_KfoldOut() for the entry points. ## End(Not run)## Not run: # CV_sgs() is called internally by forestsearch_KfoldOut(). # See forestsearch_Kfold() and forestsearch_KfoldOut() for the entry points. ## End(Not run)
Formats the detailed output from forestsearch_KfoldOut(outall=TRUE)
into publication-ready gt tables. This includes ITT estimates, original subgroup
estimates, and K-fold subgroup estimates.
cv_summary_tables( kfold_out, title = "Cross-Validation Summary", subtitle = NULL, show_metrics = TRUE, digits = 3, font_size = 12, use_gt = TRUE )cv_summary_tables( kfold_out, title = "Cross-Validation Summary", subtitle = NULL, show_metrics = TRUE, digits = 3, font_size = 12, use_gt = TRUE )
kfold_out |
List. Result from |
title |
Character. Main title for combined table. Default: "Cross-Validation Summary". |
subtitle |
Character. Subtitle for table. Default: NULL (auto-generated). |
show_metrics |
Logical. Include agreement and finding metrics in output. Default: TRUE. |
digits |
Integer. Decimal places for numeric formatting. Default: 3. |
font_size |
Integer. Font size in pixels. Default: 12. |
use_gt |
Logical. Return gt table if TRUE, data.frame if FALSE. Default: TRUE. |
If use_gt = TRUE, returns a list with gt table objects:
combined_table: Combined ITT and subgroup estimates
itt_table: ITT estimates only
original_table: Original full-data subgroup estimates
kfold_table: K-fold subgroup estimates
metrics_table: Agreement and finding metrics (if show_metrics = TRUE)
If use_gt = FALSE, returns equivalent data.frames.
cv_metrics_tables for formatting forestsearch_tenfold() results
## Not run: # Run K-fold CV cv_results <- forestsearch_Kfold(fs.est = fs_result, Kfolds = 10) # Get detailed output kfold_out <- forestsearch_KfoldOut(cv_results, outall = TRUE) # Create summary tables cv_tables <- cv_summary_tables(kfold_out) cv_tables$combined_table cv_tables$metrics_table ## End(Not run)## Not run: # Run K-fold CV cv_results <- forestsearch_Kfold(fs.est = fs_result, Kfolds = 10) # Get detailed output kfold_out <- forestsearch_KfoldOut(cv_results, outall = TRUE) # Create summary tables cv_tables <- cv_summary_tables(kfold_out) cv_tables$combined_table cv_tables$metrics_table ## End(Not run)
Generates a compact text string summarizing CV results, suitable for annotations in plots or reports.
cv_summary_text( cv_result, est.scale = "hr", include_finding = TRUE, include_agreement = TRUE )cv_summary_text( cv_result, est.scale = "hr", include_finding = TRUE, include_agreement = TRUE )
cv_result |
List. Result from |
est.scale |
Character. "hr" or "1/hr" to determine label orientation. Default: "hr". |
include_finding |
Logical. Include subgroup finding rate. Default: TRUE. |
include_agreement |
Logical |
Character string with formatted CV metrics.
## Not run: cv_text <- cv_summary_text(tenfold_results) # Returns: "CV found = 95%, Agree(+,-) = 88%, 92%" ## End(Not run)## Not run: cv_text <- cv_summary_text(tenfold_results) # Returns: "CV found = 95%, Agree(+,-) = 88%, 92%" ## End(Not run)
Computes from the
Fisher information of the logistic regression treatment coefficient.
d_eff_binary(n_sg, p_event)d_eff_binary(n_sg, p_event)
n_sg |
Integer. Subgroup sample size. |
p_event |
Numeric. Event probability (0–1), averaged across arms. |
For the model with
balanced arms ():
Numeric. Effective information.
d_eff_binary(n_sg = 100, p_event = 0.30) # 21d_eff_binary(n_sg = 100, p_event = 0.30) # 21
Computes from the
Fisher information of the Gaussian linear model treatment coefficient.
d_eff_continuous(n_sg, sigma_y)d_eff_continuous(n_sg, sigma_y)
n_sg |
Integer. Subgroup sample size. |
sigma_y |
Numeric. Residual standard deviation of the outcome. |
For the model ,
, with balanced arms:
Numeric. Effective information.
d_eff_continuous(n_sg = 100, sigma_y = 1.5) # 44.4d_eff_continuous(n_sg = 100, sigma_y = 1.5) # 44.4
Computes where is the total
expected events and is the dispersion parameter.
d_eff_count( n_sg = NULL, event_rate = NULL, total_events = NULL, dispersion = 1 )d_eff_count( n_sg = NULL, event_rate = NULL, total_events = NULL, dispersion = 1 )
n_sg |
Integer. Subgroup sample size. Required if
|
event_rate |
Numeric. Mean events per patient. Required if
|
total_events |
Numeric. Total observed events |
dispersion |
Numeric. Overdispersion parameter (1.0 for Poisson, greater than 1 for quasi-Poisson). Default: 1.0. |
For Poisson regression with log-offset:
Under proportional hazards, (Cox events), so
(Laird and Olivier, 1981).
Numeric. Effective information.
# From total events d_eff_count(total_events = 55) # From sample size and rate d_eff_count(n_sg = 100, event_rate = 0.8) # Quasi-Poisson with overdispersion d_eff_count(total_events = 55, dispersion = 1.5)# From total events d_eff_count(total_events = 55) # From sample size and rate d_eff_count(n_sg = 100, event_rate = 0.8) # Quasi-Poisson with overdispersion d_eff_count(total_events = 55, dispersion = 1.5)
Computes where is the
censoring proportion.
d_eff_survival(n_sg, prop_cens)d_eff_survival(n_sg, prop_cens)
n_sg |
Integer. Subgroup sample size. |
prop_cens |
Numeric. Proportion censored (0–1). |
Numeric. Effective information (expected events).
d_eff_survival(n_sg = 60, prop_cens = 0.45) # 33d_eff_survival(n_sg = 60, prop_cens = 0.45) # 33
dina() is the high-level user-facing wrapper around dina_fit().
It accepts a data frame plus column-name arguments, matching the
conventions used elsewhere in the forestsearch package, and
extracts the covariate matrix, response, and treatment indicator
before dispatching to dina_fit().
dina( df, outcome, treatment, covariates, family = c("gaussian", "binomial", "poisson", "cox"), status = NULL, ... )dina( df, outcome, treatment, covariates, family = c("gaussian", "binomial", "poisson", "cox"), status = NULL, ... )
df |
data frame (or tibble / |
outcome |
character(1); the name of the response column in
|
treatment |
character(1); the name of the binary treatment
indicator column in |
covariates |
character vector of length |
family |
one of |
status |
character(1) or |
... |
further arguments forwarded to |
All other arguments are forwarded to dina_fit() via ...,
including propensity_method, baseline_method, cross_fitting,
n_folds, cens_type, cens_params, n_grid, eps, and seed.
See dina_fit() for their full documentation.
Covariate columns must be numeric (or integer). Convert factors to
dummy variables (e.g., with stats::model.matrix()) before calling
if needed. Rows with any NA value in the referenced columns are
rejected with an error; use stats::na.omit() or impute beforehand.
An object of class "dina", as returned by dina_fit().
The $call component is overwritten with the original dina()
call so that print() and summary() show the high-level
invocation rather than the internal matrix-based dispatch.
dina_fit() for the underlying matrix-based estimator;
dina_bagged() for the bagged version with infinitesimal-jackknife
variance.
set.seed(1) n <- 400 df_demo <- data.frame( y = NA, w = stats::rbinom(n, 1, 0.5), x1 = stats::runif(n, -1, 1), x2 = stats::runif(n, -1, 1), x3 = stats::runif(n, -1, 1) ) tau_x <- 0.5 + 0.8 * df_demo$x1 - 0.3 * df_demo$x2 df_demo$y <- 1 + df_demo$x1 + df_demo$w * tau_x + stats::rnorm(n) fit <- dina( df = df_demo, outcome = "y", treatment = "w", covariates = c("x1", "x2", "x3"), family = "gaussian", seed = 1L ) coef(fit) confint(fit)set.seed(1) n <- 400 df_demo <- data.frame( y = NA, w = stats::rbinom(n, 1, 0.5), x1 = stats::runif(n, -1, 1), x2 = stats::runif(n, -1, 1), x3 = stats::runif(n, -1, 1) ) tau_x <- 0.5 + 0.8 * df_demo$x1 - 0.3 * df_demo$x2 df_demo$y <- 1 + df_demo$x1 + df_demo$w * tau_x + stats::rnorm(n) fit <- dina( df = df_demo, outcome = "y", treatment = "w", covariates = c("x1", "x2", "x3"), family = "gaussian", seed = 1L ) coef(fit) confint(fit)
dina_bagged() is the high-level user-facing wrapper around
dina_fit_bagged(). It accepts a data frame plus column-name
arguments and dispatches to dina_fit_bagged(), which fits the
DINA estimator on n_bags bootstrap replicates and returns the
infinitesimal-jackknife variance per Wager, Hastie and Efron (2014).
dina_bagged( df, outcome, treatment, covariates, family = c("gaussian", "binomial", "poisson", "cox"), status = NULL, ... )dina_bagged( df, outcome, treatment, covariates, family = c("gaussian", "binomial", "poisson", "cox"), status = NULL, ... )
df |
data frame (or tibble / |
outcome |
character(1); the name of the response column in
|
treatment |
character(1); the name of the binary treatment
indicator column in |
covariates |
character vector of length |
family |
one of |
status |
character(1) or |
... |
further arguments forwarded to |
All bagging-related arguments (n_bags, cross_fitting_per_bag,
n_folds_per_bag, parallel, ij_finite_sample_correction,
project_psd, verbose, ...) are forwarded via .... See
dina_fit_bagged() for full documentation.
For parallel = "bags", set future::plan() to a parallel backend
before calling dina_bagged(). Restore with
future::plan("sequential"); gc() afterwards.
An object of class c("dina_bagged", "dina"), as returned
by dina_fit_bagged(). The $call component is overwritten with
the original dina_bagged() call.
dina_fit_bagged() for the underlying matrix-based bagged
estimator; dina() for the single-pass DINA with sandwich variance.
set.seed(1) n <- 400 df_demo <- data.frame( y = NA, w = stats::rbinom(n, 1, 0.5), x1 = stats::runif(n, -1, 1), x2 = stats::runif(n, -1, 1), x3 = stats::runif(n, -1, 1) ) tau_x <- 0.5 + 0.8 * df_demo$x1 - 0.3 * df_demo$x2 df_demo$y <- 1 + df_demo$x1 + df_demo$w * tau_x + stats::rnorm(n) fit <- dina_bagged( df = df_demo, outcome = "y", treatment = "w", covariates = c("x1", "x2", "x3"), family = "gaussian", n_bags = 30L, seed = 1L ) coef(fit) sqrt(diag(vcov(fit)))set.seed(1) n <- 400 df_demo <- data.frame( y = NA, w = stats::rbinom(n, 1, 0.5), x1 = stats::runif(n, -1, 1), x2 = stats::runif(n, -1, 1), x3 = stats::runif(n, -1, 1) ) tau_x <- 0.5 + 0.8 * df_demo$x1 - 0.3 * df_demo$x2 df_demo$y <- 1 + df_demo$x1 + df_demo$w * tau_x + stats::rnorm(n) fit <- dina_bagged( df = df_demo, outcome = "y", treatment = "w", covariates = c("x1", "x2", "x3"), family = "gaussian", n_bags = 30L, seed = 1L ) coef(fit) sqrt(diag(vcov(fit)))
dina_fit() implements the two-step DINA (DIfference in NAtural parameters)
estimator of Gao and Hastie (2025) for heterogeneous treatment effects
(HTE) with general response types: Gaussian (mean difference), binomial
(log odds ratio), Poisson (log mean ratio), and Cox (log hazard ratio).
The treatment effect is assumed to be linear in the covariates,
following Proposition 1 of the paper. The procedure has two steps:
(1) estimate nuisance functions and on a training
fold, and (2) plug those estimators into a GLM (or Cox) likelihood on the
held-out fold to estimate . Cross-fitting averages the two
estimators obtained by swapping the folds.
dina_fit( X, Y, W, family = c("gaussian", "binomial", "poisson", "cox"), propensity_method = "logistic", baseline_method = NULL, cross_fitting = TRUE, n_folds = 2L, cens_type = NULL, cens_params = list(), n_grid = 500L, eps = 1e-06, seed = NULL )dina_fit( X, Y, W, family = c("gaussian", "binomial", "poisson", "cox"), propensity_method = "logistic", baseline_method = NULL, cross_fitting = TRUE, n_folds = 2L, cens_type = NULL, cens_params = list(), n_grid = 500L, eps = 1e-06, seed = NULL )
X |
a numeric covariate matrix or data frame with |
Y |
the response. For |
W |
binary treatment indicator (0 = control, 1 = treatment), length
|
family |
character; one of |
propensity_method |
how to estimate the propensity score
|
baseline_method |
how to estimate the natural-parameter functions
|
cross_fitting |
logical; if |
n_folds |
positive integer giving the number of cross-fitting folds
(default |
cens_type |
for |
cens_params |
named list of censoring parameters. Required entries
depend on
|
n_grid |
positive integer giving the number of time-grid points used
for numerical integration of the not-censoring probability for
|
eps |
small positive constant added to denominators for numerical
stability (default |
seed |
optional integer seed for the random cross-fitting fold assignment. When supplied, the global RNG state is restored on exit. |
The Gaussian family uses the simplification of Section 3.2: and , so the conditional outcome means
, are not required separately. For the
binomial and Poisson families, separate within-arm fits to
and are used to construct via the modified
propensity in Eq. (12) of the paper. For the Cox family, uses
the not-censoring probabilities of Eq. (18); how those are estimated is
controlled by cens_type.
Variance estimation follows the standard sandwich form:
, where and
are the bread (negative score derivative) and meat
(outer product of scores) matrices. Under cross-fitting these are
averaged across folds before the sandwich is formed. For the Cox step 2
we use the partial-likelihood information-matrix variance returned by
survival::coxph().
An object of class "dina", a list with components:
named numeric vector of estimated DINA coefficients
, with (Intercept) first followed by one entry per
covariate.
estimated variance-covariance matrix of .
the response family, as supplied.
sample size and number of covariates.
cross-fitting settings.
the matched call.
Gao, Z. and Hastie, T. (2025). Estimating heterogeneous treatment effects for general responses. Biometrics 81(4): ujaf162. doi:10.1093/biomtc/ujaf162.
Robinson, P. M. (1988). Root-n-consistent semiparametric regression. Econometrica 56(4): 931–954.
Nie, X. and Wager, S. (2021). Quasi-oracle estimation of heterogeneous treatment effects. Biometrika 108(2): 299–319.
Dandl, S., Bender, A. and Hothorn, T. (2024). Heterogeneous treatment effect estimation for observational data using model-based forests. Statistical Methods in Medical Research 33(3): 392–413.
set.seed(1) n <- 400 d <- 3 X <- matrix(stats::runif(n * d, -1, 1), n, d) colnames(X) <- paste0("x", seq_len(d)) W <- stats::rbinom(n, 1, stats::plogis(0.5 * X[, 1])) tau_x <- 0.5 + 0.8 * X[, 1] - 0.3 * X[, 2] Y <- 1 + X[, 1] + W * tau_x + stats::rnorm(n) fit <- dina_fit(X, Y, W, family = "gaussian", seed = 1) coef(fit) confint(fit)set.seed(1) n <- 400 d <- 3 X <- matrix(stats::runif(n * d, -1, 1), n, d) colnames(X) <- paste0("x", seq_len(d)) W <- stats::rbinom(n, 1, stats::plogis(0.5 * X[, 1])) tau_x <- 0.5 + 0.8 * X[, 1] - 0.3 * X[, 2] Y <- 1 + X[, 1] + W * tau_x + stats::rnorm(n) fit <- dina_fit(X, Y, W, family = "gaussian", seed = 1) coef(fit) confint(fit)
dina_fit_bagged() fits the DINA estimator on n_bags bootstrap
replicates of the data and aggregates the resulting per-bag coefficient
vectors into a bagged estimator. The variance is estimated by the
infinitesimal jackknife (IJ) of Wager, Hastie and Efron (2014), with
the finite-sample-corrected form
where is the empirical covariance of the
per-bag coefficients. Negative eigenvalues introduced by the
Monte-Carlo correction (an artifact of small n_bags) are optionally
clipped to zero via PSD projection so that downstream confint() is
always well-defined.
dina_fit_bagged( X, Y, W, family = c("gaussian", "binomial", "poisson", "cox"), propensity_method = "logistic", baseline_method = NULL, n_bags = 100L, cross_fitting_per_bag = TRUE, n_folds_per_bag = 2L, cens_type = NULL, cens_params = list(), n_grid = 500L, ij_finite_sample_correction = TRUE, project_psd = TRUE, eps = 1e-06, seed = NULL, parallel = c("none", "bags"), verbose = FALSE )dina_fit_bagged( X, Y, W, family = c("gaussian", "binomial", "poisson", "cox"), propensity_method = "logistic", baseline_method = NULL, n_bags = 100L, cross_fitting_per_bag = TRUE, n_folds_per_bag = 2L, cens_type = NULL, cens_params = list(), n_grid = 500L, ij_finite_sample_correction = TRUE, project_psd = TRUE, eps = 1e-06, seed = NULL, parallel = c("none", "bags"), verbose = FALSE )
X |
a numeric covariate matrix or data frame with |
Y |
the response. For |
W |
binary treatment indicator (0 = control, 1 = treatment), length
|
family |
character; one of |
propensity_method |
how to estimate the propensity score
|
baseline_method |
how to estimate the natural-parameter functions
|
n_bags |
positive integer giving the number of bootstrap bags
(default |
cross_fitting_per_bag |
logical; if |
n_folds_per_bag |
positive integer giving the number of
cross-fitting folds within each bag (default |
cens_type |
for |
cens_params |
named list of censoring parameters. Required entries
depend on
|
n_grid |
positive integer giving the number of time-grid points used
for numerical integration of the not-censoring probability for
|
ij_finite_sample_correction |
logical; if |
project_psd |
logical; if |
eps |
small positive constant added to denominators for numerical
stability (default |
seed |
optional integer seed for the random cross-fitting fold assignment. When supplied, the global RNG state is restored on exit. |
parallel |
character, one of |
verbose |
logical; if |
The implementation calls dina_fit() once per bag, with cross-fitting
optionally enabled within each bag. All other arguments
(propensity_method, baseline_method, cens_type, cens_params,
n_grid, eps) are passed through unchanged.
An object of class c("dina_bagged", "dina"), a list
containing all the components of a "dina" fit (so that the standard
S3 methods work via inheritance) plus:
the corrected, PSD-projected IJ variance matrix
(or the raw IJ if project_psd = FALSE).
the IJ variance without the finite-sample correction.
the IJ variance with the finite-sample correction, before PSD projection.
the empirical bootstrap covariance of the per-bag
coefficients, .
n_bags_used by (d + 1) matrix of
per-bag coefficient vectors.
n by n_bags_used integer matrix of bag
inclusion counts, .
requested and successfully-fit bag counts.
bag-level cross-fitting settings.
Wager, S., Hastie, T. and Efron, B. (2014). Confidence intervals for random forests: the jackknife and the infinitesimal jackknife. Journal of Machine Learning Research 15: 1625–1651.
Gao, Z. and Hastie, T. (2025). Estimating heterogeneous treatment effects for general responses. Biometrics 81(4): ujaf162. doi:10.1093/biomtc/ujaf162.
dina_fit() for the single-pass DINA estimator with
sandwich variance.
set.seed(1) n <- 400 d <- 3 X <- matrix(stats::runif(n * d, -1, 1), n, d) colnames(X) <- paste0("x", seq_len(d)) W <- stats::rbinom(n, 1, stats::plogis(0.5 * X[, 1])) tau_x <- 0.5 + 0.8 * X[, 1] - 0.3 * X[, 2] Y <- 1 + X[, 1] + W * tau_x + stats::rnorm(n) fit <- dina_fit_bagged(X, Y, W, family = "gaussian", n_bags = 30L, seed = 1L) coef(fit) sqrt(diag(vcov(fit)))set.seed(1) n <- 400 d <- 3 X <- matrix(stats::runif(n * d, -1, 1), n, d) colnames(X) <- paste0("x", seq_len(d)) W <- stats::rbinom(n, 1, stats::plogis(0.5 * X[, 1])) tau_x <- 0.5 + 0.8 * X[, 1] - 0.3 * X[, 2] Y <- 1 + X[, 1] + W * tau_x + stats::rnorm(n) fit <- dina_fit_bagged(X, Y, W, family = "gaussian", n_bags = 30L, seed = 1L) coef(fit) sqrt(diag(vcov(fit)))
Runs the same univariate (covariate, direction, threshold) search as
dina_subgroup(), but instead of selecting one subgroup it returns a
pool of per-covariate Pareto-non-dominated cuts. Each cut is
rendered as a forestsearch cut expression ("x1 <= 0.5"), so the pool
can be fed to forestsearch() as screening-stage candidates in the
same way GRF tree cuts are. This is the DINA analog of GRF candidate
discovery.
dina_frontier( fit, df, covariates, scope = c("wide", "harm"), m_diff = NULL, n_min = 60L, direction = c("both", "left", "right"), max_per_covariate = Inf, max_subgroups = Inf, digits = 3L )dina_frontier( fit, df, covariates, scope = c("wide", "harm"), m_diff = NULL, n_min = 60L, direction = c("both", "left", "right"), max_per_covariate = Inf, max_subgroups = Inf, digits = 3L )
fit |
a fitted DINA object (class |
df |
data frame containing the covariate columns. |
covariates |
character vector of numeric covariate columns to search over. |
scope |
one of |
m_diff |
scalar harm floor on the link (natural-parameter) scale.
Required when |
n_min |
positive integer minimum subgroup size. Default |
direction |
one of |
max_per_covariate |
positive integer, or |
max_subgroups |
positive integer, or Both caps default to |
digits |
significant figures for rounding the emitted threshold
in |
Why per-covariate and not a single global frontier. The global Pareto frontier in (effect, N) space collapses onto whichever single covariate dominates the trade-off boundary, returning many micro-stepped cuts on that one covariate. That is faithful to the definition but the opposite of what forestsearch needs: its value is in composing cuts drawn from several covariates. So this function computes a separate frontier for each covariate, then pools them, mirroring how a GRF tree splits on multiple variables.
Two caps, each optional. Both caps act on this function's return
value and nothing else: they trim the frontier table of single cuts
that is returned here. They are not a cap on a selection family – the
rows they trim are univariate cuts, never conjunctions – and
dina_subgroup() does not take them, so nothing they do reaches the
subgroup selector. Either may be Inf to disable it:
max_per_covariate bounds how many cuts any single covariate
contributes (its top cuts by effect), limiting within-covariate
redundancy.
max_subgroups bounds the total pool size across covariates.
Under forestsearch(subgroup_method = "dina") neither cap has any effect:
that path selects with dina_subgroup() and never consults a frontier
table. They bite only where this function's output is used directly –
including forestsearch(use_dina = TRUE, dina_args = list(selected_only = FALSE)), where cut_expr becomes the screening-stage candidate pool.
When the pool exceeds max_subgroups, the global trim is round-robin
on within-covariate effect rank – every covariate's best cut first
(ordered by effect), then every covariate's second-best, and so on –
so a finite budget preserves cross-covariate spread instead of
refilling with one covariate's micro-steps. Setting
max_per_covariate = Inf recovers a pure global budget; setting
max_subgroups = Inf recovers per-covariate-only limits. Both default to
Inf, and a finite cap that actually removes rows warns rather than
trimming the display silently.
Cut form. The expression is always the canonical "<covariate> <= <threshold>". forestsearch's factor machinery exposes both the
cut and its complement, so one expression covers both of DINA's
left/right directions – the direction column is retained for
reference only. Thresholds are rounded to digits significant
figures so they deduplicate cleanly against existing GRF / median cuts
and read well; raw values remain in threshold.
These cuts are candidates, not forced selections: forestsearch
composes (up to maxk) and consistency-gatekeeps them, exactly as it
does GRF cuts. To use them without disturbing any user-supplied
forced cuts, append rather than overwrite, e.g.
forestsearch(..., conf_force = c(my_forced_cuts, fr$cut_expr)).
A data frame (one row per cut, after the caps and threshold-rounding deduplication) with columns:
the subgroup definition (raw threshold).
subgroup size.
subgroup-mean tau-hat on the link scale.
the same on the natural scale (exponentiated for ratio families binomial/poisson/cox), used for the frontiers and the effect ranking.
the canonical forestsearch cut expression.
The result carries attributes n_frontier (total per-covariate
frontier size, deduped, before the caps), n_qualifying, and
scope. Returns a 0-row data frame (same columns) when no candidate
qualifies.
dina_subgroup() for the single-subgroup selector;
forestsearch() for the screening stage that consumes the cuts;
compute_pareto_frontier() for the post-hoc frontier reporting on a
forestsearch result.
set.seed(1) n <- 400 df_demo <- data.frame( w = stats::rbinom(n, 1, 0.5), x1 = stats::runif(n, -1, 1), x2 = stats::runif(n, -1, 1), x3 = stats::runif(n, -1, 1) ) tau_x <- 0.3 + 1.2 * df_demo$x1 - 0.4 * df_demo$x2 df_demo$y <- 0.5 * df_demo$x1 + df_demo$w * tau_x + stats::rnorm(n) fit <- dina(df_demo, outcome = "y", treatment = "w", covariates = c("x1", "x2", "x3"), family = "gaussian", seed = 1L) fr <- dina_frontier(fit, df_demo, covariates = c("x1", "x2", "x3"), n_min = 60L) fr fr$cut_expr # ready to append to forestsearch(conf_force = ...)set.seed(1) n <- 400 df_demo <- data.frame( w = stats::rbinom(n, 1, 0.5), x1 = stats::runif(n, -1, 1), x2 = stats::runif(n, -1, 1), x3 = stats::runif(n, -1, 1) ) tau_x <- 0.3 + 1.2 * df_demo$x1 - 0.4 * df_demo$x2 df_demo$y <- 0.5 * df_demo$x1 + df_demo$w * tau_x + stats::rnorm(n) fit <- dina(df_demo, outcome = "y", treatment = "w", covariates = c("x1", "x2", "x3"), family = "gaussian", seed = 1L) fr <- dina_frontier(fit, df_demo, covariates = c("x1", "x2", "x3"), n_min = 60L) fr fr$cut_expr # ready to append to forestsearch(conf_force = ...)
Given a fitted DINA object ("dina" or "dina_bagged") and a data
frame containing the covariates the model was fit on, search over
(covariate, threshold, direction) candidates for subgroups whose mean
per-patient HTE estimate meets the harm threshold m_diff, then select
one according to sg_focus (default "maxSG": the largest qualifying
subgroup). This is the simplest bridge between DINA's continuous
estimator and
forestsearch's interpretable signature framework.
dina_subgroup( fit, df, covariates, m_diff, n_min = 60L, n_min.frac = 0.1, direction = c("both", "left", "right"), max_depth = 2L, grid_probs = seq(0.1, 0.9, by = 0.1), sg_focus = "maxSG", selection_rule = "neighborhood", effect_neighborhood = 0.1, alpha = 0.05, tau_sign = 1 )dina_subgroup( fit, df, covariates, m_diff, n_min = 60L, n_min.frac = 0.1, direction = c("both", "left", "right"), max_depth = 2L, grid_probs = seq(0.1, 0.9, by = 0.1), sg_focus = "maxSG", selection_rule = "neighborhood", effect_neighborhood = 0.1, alpha = 0.05, tau_sign = 1 )
fit |
a fitted DINA object of class |
df |
data frame containing the covariate columns referenced by
|
covariates |
character vector of column names in |
m_diff |
scalar harm threshold on the natural-parameter scale
of It is a candidacy floor, not a ranking key. It decides which
candidates enter the qualifying family; Scale. The link (tau) scale of Under Under |
n_min |
positive integer, or |
n_min.frac |
numeric in (0, 1); fraction of |
direction |
one of |
max_depth |
integer, |
grid_probs |
numeric vector of probabilities in |
sg_focus |
character; subgroup selection criterion applied to the
qualifying candidates. One of Five spellings, one rule. On this path
|
selection_rule |
character; rule defining the candidate inclusion
band for |
effect_neighborhood |
numeric in |
alpha |
confidence level for the Wald interval on the
subgroup-mean tau-hat. Default |
tau_sign |
|
For each covariate x_j in covariates, the search visits the
sorted unique values of df[[x_j]] as candidate thresholds q and
for each threshold considers the left-tail subgroup of patients with
(and optionally also the right-tail subgroup
with ). A candidate
subgroup must satisfy:
subgroup size |S| >= n_min, AND
mean per-patient tau-hat over S >= m_diff.
Among the qualifying candidates, sg_focus selects the returned
subgroup, mirroring forestsearch()'s selection vocabulary:
"maxSG" (default) – largest qualifying subgroup (legacy
behaviour; size ties broken by the most extreme mean tau-hat).
"minSG" – smallest qualifying subgroup.
"eff" – most extreme effect, ignoring size.
"effMaxSG" – largest subgroup in the candidate inclusion band
(see selection_rule).
"effMinSG" – smallest subgroup in the inclusion band.
The "eff*" band foci reuse the inclusion-band logic shared with
sort_subgroups(); for ratio families (binomial, poisson, cox) the
band is applied on the natural (exponentiated) effect scale.
selection_rule defines the band, exactly as in forestsearch():
"neighborhood" (1-D effect band within effect_neighborhood of the
maximum qualifying effect), "pareto" (the 2-D non-dominated frontier
in (effect, N) space), or "both" (their intersection). DINA's
univariate (covariate, threshold, direction) candidates populate the
(effect, N) plane just as forestsearch's signatures do, so the
frontier is defined analogously.
An object of class "dina_subgroup", a list with components:
logical; TRUE if any candidate satisfied
m_diff and n_min, otherwise FALSE. When FALSE, the
subgroup-specific components below are NULL.
the chosen cut(s). Length
1 for a single-covariate subgroup; length 2 (one entry per
covariate) when max_depth = 2L selects a conjunction.
character vector of the chosen {var op q} factor
label(s), in the form consumed by get_dfpred() (AND-composed).
Length matches covariate.
a single combined display string, e.g.
"{nodes >= 10 & age >= 60}".
integer; number of covariates in the selected
subgroup (1 or 2).
integer size of the chosen subgroup.
scalar subgroup-mean tau-hat (in the
tau_sign orientation).
Wald standard error, computed as
sqrt(a_S^T vcov(fit) a_S) (CONDITIONAL on the chosen
subgroup – not selection-adjusted).
named length-2 vector (lower, upper) giving the
Wald 1 - alpha CI.
logical vector of length nrow(df) marking which
rows are in the chosen subgroup.
the inputs / fit family, echoed for reproducibility.
nrow(df).
total number of (j, dir, q)
triples evaluated.
number of those triples that met
both the size and m_diff constraints.
the matched call.
With max_depth = 2 the candidate set is extended to AND-conjunctions
of two thresholds on distinct covariates, e.g.
nodes >= q1 & age >= q2. Because tau-hat is linear in X, the mean
over the intersection mask is scored exactly like a depth-1 cut, so
the m_diff, n_min, and sg_focus machinery is unchanged. To stay
tractable, depth-2 pairs are generated over a per-covariate quantile
grid (grid_probs) rather than every unique value, while depth-1
singletons retain full resolution. Singletons and pairs are ranked
jointly under sg_focus, so a depth-2 conjunction is returned only
when it outranks every depth-1 candidate; otherwise the depth-1
selection is recovered exactly. When the selected subgroup is a
conjunction, covariate, direction, and threshold are
length-2 vectors and labels holds the two {var op q} factors in
the form consumed by get_dfpred().
The variance of the subgroup-mean tau-hat is computed as
with and
.
The variance source therefore tracks the fit class: sandwich for
"dina" and infinitesimal jackknife for "dina_bagged".
The returned Wald confidence interval is conditional on the
selected (covariate, threshold) being treated as pre-specified.
It does not adjust for the selection across the candidate set and
will undercover when the search space is large. Use this CI for
descriptive reporting; for hypothesis testing against m_diff,
a selection-adjusted interval via bootstrap of the full search
procedure is required. The undercoverage is more pronounced with
max_depth = 2L, whose candidate space is substantially larger.
dina_fit() / dina() for the underlying estimator;
dina_fit_bagged() / dina_bagged() for the bagged variant whose
IJ variance propagates through the subgroup-mean SE here;
sort_subgroups() and forestsearch() for the selection
vocabulary mirrored by sg_focus.
set.seed(1) n <- 400 df_demo <- data.frame( w = stats::rbinom(n, 1, 0.5), x1 = stats::runif(n, -1, 1), x2 = stats::runif(n, -1, 1), x3 = stats::runif(n, -1, 1) ) tau_x <- 0.3 + 1.2 * df_demo$x1 - 0.4 * df_demo$x2 df_demo$y <- 0.5 * df_demo$x1 + df_demo$w * tau_x + stats::rnorm(n) fit <- dina(df_demo, outcome = "y", treatment = "w", covariates = c("x1", "x2", "x3"), family = "gaussian", seed = 1L) sg <- dina_subgroup(fit, df_demo, covariates = c("x1", "x2", "x3"), m_diff = 0.5, n_min = 60L) sg # Tighter subgroup via the effect-neighborhood band: dina_subgroup(fit, df_demo, covariates = c("x1", "x2", "x3"), m_diff = 0.5, n_min = 60L, sg_focus = "effMaxSG") # Frontier-aware selection (2-D non-dominated set in (effect, N)): dina_subgroup(fit, df_demo, covariates = c("x1", "x2", "x3"), m_diff = 0.5, n_min = 60L, sg_focus = "effMaxSG", selection_rule = "both") # Depth-2: allow AND-conjunctions of two covariates. dina_subgroup(fit, df_demo, covariates = c("x1", "x2", "x3"), m_diff = 0.5, n_min = 60L, max_depth = 2L)set.seed(1) n <- 400 df_demo <- data.frame( w = stats::rbinom(n, 1, 0.5), x1 = stats::runif(n, -1, 1), x2 = stats::runif(n, -1, 1), x3 = stats::runif(n, -1, 1) ) tau_x <- 0.3 + 1.2 * df_demo$x1 - 0.4 * df_demo$x2 df_demo$y <- 0.5 * df_demo$x1 + df_demo$w * tau_x + stats::rnorm(n) fit <- dina(df_demo, outcome = "y", treatment = "w", covariates = c("x1", "x2", "x3"), family = "gaussian", seed = 1L) sg <- dina_subgroup(fit, df_demo, covariates = c("x1", "x2", "x3"), m_diff = 0.5, n_min = 60L) sg # Tighter subgroup via the effect-neighborhood band: dina_subgroup(fit, df_demo, covariates = c("x1", "x2", "x3"), m_diff = 0.5, n_min = 60L, sg_focus = "effMaxSG") # Frontier-aware selection (2-D non-dominated set in (effect, N)): dina_subgroup(fit, df_demo, covariates = c("x1", "x2", "x3"), m_diff = 0.5, n_min = 60L, sg_focus = "effMaxSG", selection_rule = "both") # Depth-2: allow AND-conjunctions of two covariates. dina_subgroup(fit, df_demo, covariates = c("x1", "x2", "x3"), m_diff = 0.5, n_min = 60L, max_depth = 2L)
Performs selection-adjusted inference for the harm subgroup discovered
by dina_subgroup(). For each of n_boot bootstrap resamples of the
input data frame, the function refits DINA via dina() and re-runs
the subgroup search via dina_subgroup(), then reports three classes
of output:
dina_subgroup_bootstrap( df, outcome, treatment, covariates, family = c("gaussian", "binomial", "poisson", "cox"), status = NULL, m_diff, n_min = 60L, n_min.frac = 0.1, direction = c("both", "left", "right"), sg_focus = "maxSG", effect_neighborhood = 0.1, alpha = 0.05, n_boot = 200L, parallel = c("none", "boots"), seed = NULL, verbose = FALSE, fit = NULL, sg = NULL, refit = TRUE, refit_strata = NULL, refit_confounders = "none", ... )dina_subgroup_bootstrap( df, outcome, treatment, covariates, family = c("gaussian", "binomial", "poisson", "cox"), status = NULL, m_diff, n_min = 60L, n_min.frac = 0.1, direction = c("both", "left", "right"), sg_focus = "maxSG", effect_neighborhood = 0.1, alpha = 0.05, n_boot = 200L, parallel = c("none", "boots"), seed = NULL, verbose = FALSE, fit = NULL, sg = NULL, refit = TRUE, refit_strata = NULL, refit_confounders = "none", ... )
df |
data frame containing the outcome, treatment, and covariate columns. |
outcome |
character(1); name of the response column in |
treatment |
character(1); name of the binary treatment indicator column. |
covariates |
character vector; names of the covariate columns to search over. All must be numeric. |
family |
one of |
status |
character(1) or |
m_diff |
scalar harm threshold on the natural-parameter scale. |
n_min |
positive integer, or |
n_min.frac |
numeric in (0, 1); fraction of |
direction |
one of |
sg_focus |
character; subgroup selection criterion, forwarded to
|
effect_neighborhood |
numeric in |
alpha |
confidence level for the percentile CI. Default |
n_boot |
positive integer; number of bootstrap iterations.
Default |
parallel |
one of |
seed |
optional integer seed for reproducibility of the
bootstrap indices. Default |
verbose |
logical; if |
fit |
optional precomputed DINA fit ( |
sg |
optional precomputed |
refit |
logical; if |
refit_strata |
|
refit_confounders |
adjustment set for the within-subgroup
model: |
... |
further arguments forwarded to |
An effect CI on a*^T beta_b, where a* = (1, x_bar_S) is
held FIXED from the original-data subgroup S* and beta_b is
each iteration's DINA coefficient vector. This is the
bootstrap analog of the Wald CI returned by
dina_subgroup() – conditional on the discovered subgroup
definition, with width reflecting sampling variance of
beta-hat.
Stability CIs on the bootstrap-selected n_subgroup and
threshold, restricted to iterations that selected the same
(covariate, direction) as the original-data subgroup.
A selection-frequency table counting how often each
(covariate, direction) was chosen. The covariate-selection
stability diagnostic.
Each bootstrap iteration calls dina() (single-pass, sandwich
variance) under the hood, not dina_bagged().
If you have already run dina() and dina_subgroup() on the original
data, pass those objects via fit and/or sg so the original-data
point estimate reuses them rather than being recomputed here. This
guarantees the reported point estimate matches your standalone result
exactly; otherwise the internal refit uses its own cross-fitting fold
assignment, which at a boundary-case m_diff can disagree with the
upstream fit on whether a subgroup qualifies.
An object of class "dina_subgroup_bootstrap", a list with
components:
the "dina_subgroup" object computed on the
original (unbootstrapped) data.
named length-2 numeric vector (lower,
upper) giving the bootstrap percentile CI on the
fixed-subgroup linear functional a*^T beta_b, where
a* = (1, x_bar_S) is the covariate-mean vector of the
original-data subgroup and beta_b is each bootstrap
iteration's DINA coefficient vector. This is the
meaningful effect-uncertainty CI; width reflects sampling
variance of beta-hat.
numeric vector of length n_boot
containing a*^T beta_b for each iteration (NA for
iterations whose DINA fit failed).
the "dina_subgroup_refit" object (standard
within-subgroup model on the original data), or NULL if
refit = FALSE, no subgroup was found, or the fit failed.
named length-2 numeric vector (lower,
upper); bootstrap percentile CI on the within-subgroup
standard-model treatment effect, refit within the FIXED
discovered signature on each resample. Conditional on the
signature; comparable to effect_ci. (NA, NA) if refit
was not performed.
numeric vector of length n_boot of
per-iteration within-signature standard-model treatment
effects (NA where not computed or the fit failed).
named length-2 numeric
vectors giving percentile CIs for the structural quantities
of bootstrap-selected subgroups, restricted to iterations
that selected the same (covariate, direction) as the
original-data subgroup. Subgroup-size and boundary-location
stability diagnostics.
integer; number of bootstrap iterations
whose selection matched the original-data
(covariate, direction). Denominator for the stability CIs.
a table of
(covariate, direction) selection counts across found
iterations. Covariate-selection stability diagnostic.
data frame with one row per bootstrap
iteration; columns found, covariate, direction,
threshold, n_subgroup, mean_tau_hat, failed. The
mean_tau_hat column records the bootstrap-selected
subgroup's boundary value (pinned near m_diff by the
search rule) and is retained for diagnostic transparency
rather than as a meaningful effect estimate.
convergence diagnostics.
echoed inputs.
the matched call.
A naive bootstrap of dina_subgroup()'s mean_tau_hat is
uninformative. Under the default sg_focus = "maxSG" the search
selects the largest subgroup whose mean tau-hat exceeds m_diff,
so the chosen subgroup lies at the boundary by construction: adding
one more patient would push the mean below m_diff. Each bootstrap
iteration's selected subgroup is similarly pinned, so the bootstrap
distribution of mean_tau_hat clusters tightly above m_diff
and the resulting CI quantifies threshold-grid granularity
rather than effect uncertainty. (Under the band foci
"effMaxSG" / "effMinSG" the per-iteration subgroup is instead
anchored to the maximum effect rather than the m_diff boundary,
but the same caveat applies: mean_tau_hat is a selection-pinned
quantity, not an effect estimate.) The fixed-subgroup linear
functional a*^T beta_b instead targets the clinically
meaningful estimand "treatment effect for patients whose
covariates lie in the discovered subgroup". Because DINA's
beta-hat is cross-fit (and hence asymptotically unbiased even
for data-driven contrasts), the percentile distribution of
a*^T beta_b gives a non-parametric CI that complements the
sandwich Wald CI of dina_subgroup().
When refit = TRUE (default), the function reports two treatment
effects for the discovered subgroup, on the same natural-parameter
scale:
the DINA effect (effect_ci): the BLP-analog
a*^T beta, a projection of the cross-fitted CATE surface
averaged over the subgroup; and
the within-subgroup standard-model effect
(refit_effect_ci): the treatment coefficient from a plain Cox
model (or GLM) fit on the subgroup, refit within the FIXED
discovered signature on each resample. This is the contrast a
clinical-trial reader usually expects.
The two coincide only under correct linearity; reporting both is
informative. Crucially, both CIs are conditional on the
discovered signature treated as pre-specified – neither corrects for
the data-driven selection of the signature. A selection-adjusted
(de-biased) interval requires the more involved bias-corrected
bootstrap of Leon et al. (2024, Section 3.2) and is not provided
here; selection stability is instead summarized by
selection_frequency.
With parallel = "boots", each bootstrap iteration runs in a worker
via foreach::foreach() %dofuture%. Set future::plan() to a
parallel backend (e.g., future::multisession) before calling, and
reset to "sequential" afterwards. Workers run from the installed
package; devtools::install() is required after code edits before
the parallel branch will see them.
dina_subgroup() for the underlying threshold search;
dina() for the per-iteration DINA fit;
dina_subgroup_refit for the standard within-subgroup
model reported when refit = TRUE.
## Not run: set.seed(1) n <- 400 df_demo <- data.frame( w = stats::rbinom(n, 1, 0.5), x1 = stats::runif(n, -1, 1), x2 = stats::runif(n, -1, 1) ) tau_x <- 0.4 + 1.2 * df_demo$x1 df_demo$y <- 0.5 * df_demo$x1 + df_demo$w * tau_x + stats::rnorm(n) bs <- dina_subgroup_bootstrap( df = df_demo, outcome = "y", treatment = "w", covariates = c("x1", "x2"), family = "gaussian", m_diff = 0.5, n_boot = 50L, seed = 1L ) print(bs) ## End(Not run)## Not run: set.seed(1) n <- 400 df_demo <- data.frame( w = stats::rbinom(n, 1, 0.5), x1 = stats::runif(n, -1, 1), x2 = stats::runif(n, -1, 1) ) tau_x <- 0.4 + 1.2 * df_demo$x1 df_demo$y <- 0.5 * df_demo$x1 + df_demo$w * tau_x + stats::rnorm(n) bs <- dina_subgroup_bootstrap( df = df_demo, outcome = "y", treatment = "w", covariates = c("x1", "x2"), family = "gaussian", m_diff = 0.5, n_boot = 50L, seed = 1L ) print(bs) ## End(Not run)
Given a subgroup discovered by dina_subgroup, fit the
standard treatment-effect model on the patients in that
subgroup: a Cox proportional-hazards model when
sg$family == "cox", or a generalized linear model otherwise.
The reported effect is the treatment coefficient on the
natural-parameter scale (log-HR for Cox, log-OR for binomial,
log-IRR for Poisson, mean difference for Gaussian), directly
comparable to the DINA effect returned by dina_subgroup() but
obtained from a conventional within-subgroup regression rather than
from the cross-fitted CATE surface.
dina_subgroup_refit( sg, df, treatment, outcome, covariates, status = NULL, strata = NULL, confounders = "none", alpha = 0.05 )dina_subgroup_refit( sg, df, treatment, outcome, covariates, status = NULL, strata = NULL, confounders = "none", alpha = 0.05 )
sg |
a |
df |
data frame the subgroup was identified on; must contain the
outcome, treatment, covariate, and any |
treatment |
character(1); name of the binary treatment column. |
outcome |
character(1); name of the response column. For
|
covariates |
character vector of the DINA covariate names. Used
only to construct the automatic adjustment set when
|
status |
character(1) or |
strata |
|
confounders |
|
alpha |
confidence level for the Wald interval. Default
|
The model is unadjusted by default (~ treatment); optional
covariate adjustment and stratification are available via
confounders and strata.
An object of class "dina_subgroup_refit", a list with
components:
treatment coefficient on the natural-parameter scale (the point estimate).
Wald standard error of effect.
named length-2 numeric vector (lower,
upper); Wald 1 - alpha CI on the
natural-parameter scale.
character label for the scale: one of
"log-HR", "log-OR", "log-IRR",
"MD".
logical; TRUE when effect is a
log-ratio (Cox/binomial/Poisson) so that exp() maps it
to HR/OR/IRR.
character; the subgroup rule, e.g.
"nodes >= 12".
the fitted model formula.
the resolved adjustment set and stratification columns actually used.
subgroup size and per-arm counts.
event counts
within the subgroup (Cox only; NA otherwise).
echoed inputs.
the fitted coxph / glm object.
the matched call.
Three modes:
"none" (default) – unadjusted: ~ treatment.
NULL – automatic: the DINA covariates supplied in
covariates, with the subgroup-defining covariate
(sg$covariate) omitted. Omitting it avoids adjusting on
a variable whose range is restricted by the subgroup definition.
a character vector – exactly those columns. A warning is
issued if it includes sg$covariate.
The Wald confidence interval treats the subgroup definition as pre-specified. It does not adjust for the data-driven selection of the signature; it is a within-signature (conditional) interval, not a selection-adjusted one. A selection-adjusted interval requires a bias-corrected bootstrap of the full search procedure (Leon et al. 2024, Section 3.2) and is not provided here.
dina_subgroup for the subgroup search and its
DINA (BLP-analog) effect; dina_subgroup_bootstrap for
bootstrap inference that reports this within-subgroup model
alongside the DINA effect.
## Not run: fit <- dina(df, outcome = "time", treatment = "trt", covariates = c("age", "nodes", "er"), family = "cox", status = "status", seed = 1L) sg <- dina_subgroup(fit, df, covariates = c("age", "nodes", "er"), m_diff = 0, n_min = 60L) # Unadjusted within-subgroup Cox model (default): dina_subgroup_refit(sg, df, treatment = "trt", outcome = "time", covariates = c("age", "nodes", "er"), status = "status") # Adjusted for DINA covariates except the subgroup-defining one: dina_subgroup_refit(sg, df, treatment = "trt", outcome = "time", covariates = c("age", "nodes", "er"), status = "status", confounders = NULL) ## End(Not run)## Not run: fit <- dina(df, outcome = "time", treatment = "trt", covariates = c("age", "nodes", "er"), family = "cox", status = "status", seed = 1L) sg <- dina_subgroup(fit, df, covariates = c("age", "nodes", "er"), m_diff = 0, n_min = 60L) # Unadjusted within-subgroup Cox model (default): dina_subgroup_refit(sg, df, treatment = "trt", outcome = "time", covariates = c("age", "nodes", "er"), status = "status") # Adjusted for DINA covariates except the subgroup-defining one: dina_subgroup_refit(sg, df, treatment = "trt", outcome = "time", covariates = c("age", "nodes", "er"), status = "status", confounders = NULL) ## End(Not run)
Dummy-code a data frame (numeric pass-through, factors expanded)
dummy_encode(df)dummy_encode(df)
df |
Data frame with numeric and/or factor columns. |
Data frame with numeric columns unchanged and factor columns
expanded via acm.disjctif.
df <- data.frame(age = c(40, 55, 70), grade = factor(c("1", "2", "3"))) dummy_encode(df)df <- data.frame(age = c(40, 55, 70), grade = factor(c("1", "2", "3"))) dummy_encode(df)
Evaluates whether enough evidence exists to stop early based on confidence interval for consistency proportion.
early_stop_decision( n_success, n_total, threshold, conf.level = 0.95, min_samples = 20 )early_stop_decision( n_success, n_total, threshold, conf.level = 0.95, min_samples = 20 )
n_success |
Integer. Number of splits meeting consistency. |
n_total |
Integer. Total number of valid splits. |
threshold |
Numeric. Target consistency threshold. |
conf.level |
Numeric. Confidence level for decision (default 0.95). |
min_samples |
Integer. Minimum samples before allowing early stop. |
Character. One of "continue", "pass", or "fail".
early_stop_decision(95, 100, threshold = 0.90) early_stop_decision(60, 100, threshold = 0.90) early_stop_decision(10, 15, threshold = 0.90) # below min_samplesearly_stop_decision(95, 100, threshold = 0.90) early_stop_decision(60, 100, threshold = 0.90) early_stop_decision(10, 15, threshold = 0.90) # below min_samples
Computes using the requested method, then
constructs both stabilized IPTW weights and the Bang & Robins (2005)
inverse propensity score (IPS) covariate. The data frame is returned
with three new columns: ps_hat (raw PS), sw (stabilized
IPTW weights), and ips_covar (IPS covariate).
estimate_propensity_scores( data, treat.name, confounders.name, method = c("grf", "lasso", "logistic", "none"), grf_forest = NULL, seed = 8316951L, trim = c(0.025, 0.975) )estimate_propensity_scores( data, treat.name, confounders.name, method = c("grf", "lasso", "logistic", "none"), grf_forest = NULL, seed = 8316951L, trim = c(0.025, 0.975) )
data |
Data frame. |
treat.name |
Character. Name of binary treatment column (0/1). |
confounders.name |
Character vector. Covariate names for the propensity model. |
method |
Character. One of |
grf_forest |
Optional. A fitted |
seed |
Integer. Random seed for GRF. |
trim |
Numeric vector of length 2. Quantile bounds for PS trimming.
Default: |
A list with:
data |
Data frame with |
ps_hat |
Numeric vector of estimated propensity scores. |
sw |
Numeric vector of stabilized IPTW weights. |
ips_covar |
Numeric vector of inverse PS covariates (Bang & Robins 2005). |
method |
Character. Method actually used. |
trimmed |
Integer. Number of observations trimmed. |
Bang H, Robins JM (2005). "Doubly Robust Estimation in Missing Data and Causal Inference Models." Biometrics 61(4): 962–973.
Hernan MA, Robins JM, Brumback B (2000). "Marginal Structural Models and Causal Inference in Epidemiology." Epidemiology 11(5): 550–560.
## Not run: df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") ps_result <- estimate_propensity_scores( data = df, treat.name = "hormon", confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), method = "grf" ) summary(ps_result$ps_hat) summary(ps_result$sw) ## End(Not run)## Not run: df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") ps_result <- estimate_propensity_scores( data = df, treat.name = "hormon", confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), method = "grf" ) summary(ps_result$ps_hat) summary(ps_result$sw) ## End(Not run)
Parses a string of the form "var op value" and evaluates it
directly against a data frame column using operator dispatch. Falls back
to column-name lookup for bare names.
evaluate_comparison(expr, df)evaluate_comparison(expr, df)
expr |
Character. An expression like |
df |
Data frame whose columns are referenced by |
Supported operators (matched longest-first to avoid partial-match
ambiguity): <=, >=, !=, ==, <,
>.
If no operator is found, expr is treated as a column name and
the result is df[[expr]] == 1.
The value on the right-hand side is coerced to numeric when possible, otherwise kept as character for string comparisons.
Logical vector of length nrow(df).
## Not run: df <- data.frame(er = c(-1, 0, 1, 2), size = c(10, 20, 30, 40)) evaluate_comparison("er <= 0", df) # [1] TRUE TRUE FALSE FALSE evaluate_comparison("size > 25", df) # [1] FALSE FALSE TRUE TRUE ## End(Not run)## Not run: df <- data.frame(er = c(-1, 0, 1, 2), size = c(10, 20, 30, 40)) evaluate_comparison("er <= 0", df) # [1] TRUE TRUE FALSE FALSE evaluate_comparison("size > 25", df) # [1] FALSE FALSE TRUE TRUE ## End(Not run)
Evaluates a single subgroup for consistency using a two-stage approach: Stage 1 screens with fewer splits, Stage 2 uses sequential batched evaluation with early stopping for efficient evaluation.
evaluate_consistency_twostage( m, index.Z, names.Z, df, found.hrs, hr.consistency, pconsistency.threshold, pconsistency.digits = 2, maxk, confs_labels, details = FALSE, n.splits.screen = 30, screen.threshold = NULL, n.splits.max = 400, batch.size = 20, conf.level = 0.95, min.valid.screen = 10, estimator_fn = NULL, consistency_threshold = NULL, adjust_covariates = NULL, consistency_method = "split", glm_resample_spec = NULL )evaluate_consistency_twostage( m, index.Z, names.Z, df, found.hrs, hr.consistency, pconsistency.threshold, pconsistency.digits = 2, maxk, confs_labels, details = FALSE, n.splits.screen = 30, screen.threshold = NULL, n.splits.max = 400, batch.size = 20, conf.level = 0.95, min.valid.screen = 10, estimator_fn = NULL, consistency_threshold = NULL, adjust_covariates = NULL, consistency_method = "split", glm_resample_spec = NULL )
m |
Integer. Index of subgroup to evaluate. |
index.Z |
data.table or matrix. Factor indicators for all subgroups. |
names.Z |
Character vector. Names of factor columns. |
df |
data.frame. Original data with Y, Event, Treat, id columns. |
found.hrs |
data.table. Subgroup hazard ratio results. |
hr.consistency |
Numeric. |
pconsistency.threshold |
Numeric. |
pconsistency.digits |
Integer. Number of decimal places to which the
consistency proportion is rounded before it is compared with
|
maxk |
Integer. Maximum number of factors in a subgroup. |
confs_labels |
Character vector. Labels for confounders. |
details |
Logical. Print progress details. |
n.splits.screen |
Integer. Number of splits for Stage 1 (default 30). |
screen.threshold |
Numeric. Screening threshold for Stage 1 (default auto-calculated). |
n.splits.max |
Integer. Maximum total splits (default 400). |
batch.size |
Integer. Splits per batch in Stage 2 (default 20). |
conf.level |
Numeric. Confidence level for early stopping (default 0.95). |
min.valid.screen |
Integer. Minimum valid splits in Stage 1 (default 10). |
estimator_fn |
Closure or |
consistency_threshold |
Numeric or |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
consistency_method |
Character. |
glm_resample_spec |
List or |
Named numeric vector with consistency results, or NULL if not met.
## Not run: # evaluate_consistency_twostage() is called internally by forestsearch() # when use_twostage = TRUE. Use forestsearch() as the entry point: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status", use_twostage = TRUE) ## End(Not run)## Not run: # evaluate_consistency_twostage() is called internally by forestsearch() # when use_twostage = TRUE. Use forestsearch() as the entry point: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status", use_twostage = TRUE) ## End(Not run)
Evaluates all cut expressions once and caches results to avoid redundant evaluation. Much faster than evaluating repeatedly.
evaluate_cuts_once(confs, df, details = FALSE)evaluate_cuts_once(confs, df, details = FALSE)
confs |
Character vector of cut expressions. |
df |
Data frame to evaluate expressions against. |
details |
Logical. Print details during execution. |
This replaces multiple eval(parse()) calls scattered throughout get_FSdata. By caching results, we avoid:
Repeated parsing of expressions
Repeated evaluation on dataframe
Redundant uniqueness checks
List with:
evaluations: List of evaluated vectors (logical TRUE/FALSE) for each cut
is_valid: Logical vector indicating which cuts produced >1 unique value
has_error: Logical vector indicating which cuts failed to evaluate
df <- data.frame(age = c(40, 55, 70), size = c(20, 30, 25)) confs <- c("age <= 50", "size > 25") evaluate_cuts_once(confs, df)df <- data.frame(age = c(40, 55, 70), size = c(20, 30, 25)) confs <- c("age <= 50", "size > 25") evaluate_cuts_once(confs, df)
Evaluates a single candidate subgroup for treatment-effect consistency using
repeated random data splits (the standard, non-two-stage algorithm). For
each split the subgroup effect is scored and compared against the harm
threshold; the consistency rate is the proportion of splits meeting it.
Called internally by forestsearch (via
subgroup.consistency); evaluate_consistency_twostage
is the screened, early-stopping counterpart used when
use_twostage = TRUE.
evaluate_subgroup_consistency( m, index.Z, names.Z, df, found.hrs, n.splits, hr.consistency, pconsistency.threshold, pconsistency.digits = 2, maxk, confs_labels, details = FALSE, estimator_fn = NULL, consistency_threshold = NULL, adjust_covariates = NULL, consistency_method = "split", glm_resample_spec = NULL, skip_consistency = FALSE )evaluate_subgroup_consistency( m, index.Z, names.Z, df, found.hrs, n.splits, hr.consistency, pconsistency.threshold, pconsistency.digits = 2, maxk, confs_labels, details = FALSE, estimator_fn = NULL, consistency_threshold = NULL, adjust_covariates = NULL, consistency_method = "split", glm_resample_spec = NULL, skip_consistency = FALSE )
m |
Integer. Index of subgroup to evaluate. |
index.Z |
data.table or matrix. Factor indicators for all subgroups. |
names.Z |
Character vector. Names of factor columns. |
df |
data.frame. Original data with Y, Event, Treat, id columns. |
found.hrs |
data.table. Subgroup hazard ratio results. |
n.splits |
Integer. Number of random splits used to evaluate consistency. |
hr.consistency |
Numeric. |
pconsistency.threshold |
Numeric. |
pconsistency.digits |
Integer. Number of decimal places to which the
consistency proportion is rounded before it is compared with
|
maxk |
Integer. Maximum number of factors in a subgroup. |
confs_labels |
Character vector. Labels for confounders. |
details |
Logical. Print progress details. |
estimator_fn |
Closure or |
consistency_threshold |
Numeric or |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
consistency_method |
Character. |
glm_resample_spec |
List or |
skip_consistency |
Logical. When |
Named numeric vector with consistency results, or NULL if the
subgroup does not meet the criteria.
## Not run: # evaluate_subgroup_consistency() is called internally by forestsearch(). # Use forestsearch() as the entry point: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") ## End(Not run)## Not run: # evaluate_subgroup_consistency() is called internally by forestsearch(). # Use forestsearch() as the entry point: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") ## End(Not run)
Expand a seed covariate frame to n_super rows via a Gaussian copula.
expand_covariates( seed_df, vars, n_super, seed = 20260903L, integer_vars = NULL, method = c("copula", "bootstrap") )expand_covariates( seed_df, vars, n_super, seed = 20260903L, integer_vars = NULL, method = c("copula", "bootstrap") )
seed_df |
data.frame of the seed trial (only |
vars |
covariates to expand (numeric or 0/1 binary) |
n_super |
rows to generate |
seed |
RNG seed |
integer_vars |
variables to round back to integers (auto-detected if NULL) |
method |
"copula" (default) or "bootstrap" (row resampling, as the package does by default – no novel covariate patterns) |
data.frame with n_super rows and columns vars
Wraps pareto_frontier_table with a verbal account of
the selection: why the chosen subgroup satisfies the configured
sg_focus + selection_rule criterion, and, for each
non-dominated alternative, on which lexicographic axis it lost to
the winner.
explain_pareto_selection( fs, ci_table = NULL, effect_label = NULL, digits = 3L, verbosity = c("medium", "compact", "detailed"), format = c("markdown", "character", "list") )explain_pareto_selection( fs, ci_table = NULL, effect_label = NULL, digits = 3L, verbosity = c("medium", "compact", "detailed"), format = c("markdown", "character", "list") )
fs |
A |
ci_table |
Optional data.table from
|
effect_label |
Optional character override for the effect-
measure label in prose, e.g. |
digits |
Integer. Decimal places for effect estimates and
bound values in the prose. Default |
verbosity |
One of |
format |
One of |
The wrapper is descriptive, not algorithmic: it reads the
frontier and the configuration off the returned fs object and
renders prose. It does not re-run the selection. A future change
to the lexicographic key in forestsearch would require
updating this wrapper to remain accurate.
Depending on format:
"markdown"A single string with embedded line
breaks and bullet markers, ready for cat() in a Quarto
chunk with results = "asis".
"character"A character vector with one element per logical paragraph or bullet (no markdown markers).
"list"A structured list with named elements
criterion, selected, losers,
closing, and meta.
"compact"Criterion sentence + selected line + a one-line bullet per loser.
"medium" (default)Criterion sentence + selected
paragraph (including the Naive CI if ci_table is given)
+ one short paragraph per loser, each naming the lex-key axis
on which the loser was beaten.
"detailed"Medium + a closing paragraph that reproduces the formal lex key, the resolved indicator values, and a note about any ties broken downstream.
Losers are sorted by "closeness-to-winning", which is operationalised
as: later loss-axis comes first, then smaller gap on
that axis. Specifically, a loser that lost on K (the last
tiebreaker) is reported before one that lost on N (an early
axis), and a loser that beat the winner on every axis until the
final one is reported before a loser that fell off the band entirely.
This puts the boundary cases first, which is the most informative
order for understanding how the rule made its choice.
pareto_frontier_table,
plot_pareto_frontier,
compute_frontier_cis.
## Not run: ci_tab <- compute_frontier_cis(fs, n_splits = 1000, seed = 1) # Inline in a Quarto chunk cat(explain_pareto_selection(fs, ci_table = ci_tab)) # Or get the structured object explain_pareto_selection(fs, ci_table = ci_tab, format = "list") ## End(Not run)## Not run: ci_tab <- compute_frontier_cis(fs, n_splits = 1000, seed = 1) # Inline in a Quarto chunk cat(explain_pareto_selection(fs, ci_table = ci_tab)) # Or get the structured object explain_pareto_selection(fs, ci_table = ci_tab, format = "list") ## End(Not run)
Returns the exact list of arguments that forestsearch() passed
to its inner GRF subgroup-identification call
(grf.subg.harm.survival() for survival outcomes,
grf.subg.harm.glm() for binary, continuous, and count
outcomes). The result is suitable for
do.call(grf.subg.harm.glm, args) or
do.call(grf.subg.harm.survival, args) to reproduce the
standalone GRF run with identical settings.
extract_grf_args(fs)extract_grf_args(fs)
fs |
A fitted |
Typical uses:
Sensitivity analyses: modify one or two arguments and re-fit
GRF without re-running the full forestsearch pipeline, e.g.
do.call(grf.subg.harm.glm,
modifyList(extract_grf_args(fs), list(maxdepth = 3L))).
Reproducibility audits: confirm what the inner GRF saw.
Independent variable-importance or policy-tree extraction from a forest fit with the same configuration.
This function reads from fs$args_call_all (populated by
forestsearch() via mget() of its formals) and from
fs$outcome_type. It is the user-facing complement to the
internal builders that the forestsearch() call sites use.
A named list of arguments. For survival outcomes the list
targets grf.subg.harm.survival; for binary,
continuous, or count outcomes it targets
grf.subg.harm.glm. The list carries an attribute
"grf_function" holding the target function name as a
character string.
forestsearch,
grf.subg.harm.glm,
grf.subg.harm.survival.
## Not run: # Reproduce the inner GRF run args <- extract_grf_args(fs) fn <- match.fun(attr(args, "grf_function")) grf_standalone <- do.call(fn, args) # Sensitivity: deeper trees, everything else unchanged args_deep <- modifyList(args, list(maxdepth = 3L)) grf_deep <- do.call(fn, args_deep) ## End(Not run)## Not run: # Reproduce the inner GRF run args <- extract_grf_args(fs) fn <- match.fun(attr(args, "grf_function")) grf_standalone <- do.call(fn, args) # Sensitivity: deeper trees, everything else unchanged args_deep <- modifyList(args, list(maxdepth = 3L)) grf_deep <- do.call(fn, args_deep) ## End(Not run)
Reduces a forestsearch_comparison object to a tidy
data.frame with one row per combo, summarizing each configuration's
selected subgroup. Intended for post-hoc aggregation across
analyses (e.g.\ side-by-side summary of a continuous-outcome
comparison and a binary-outcome comparison in a single document).
extract_selected_subgroups(x, analysis_label = NULL)extract_selected_subgroups(x, analysis_label = NULL)
x |
A |
analysis_label |
Optional character scalar identifying the
parent analysis (e.g.\ |
A data.frame with columns:
analysisIf analysis_label is supplied;
otherwise omitted.
comboInteger index, 1..n_combos.
combo_labelPretty label (e.g.\
"effMaxSG / pareto").
sg_focus, selection_rule
The two comparison axes.
outcome_type, effect_measure
Recorded
per row from fs$outcome_type /
fs$effect_measure, so mixed-effect-measure runs
remain comparable.
subgroupSelected subgroup definition –
NA_character_ if no subgroup was identified;
otherwise paste(fs$sg.harm, collapse = " & ").
N_H, N_Hc
Sample sizes in the harm
subgroup H (treat.recommend == 0) and its complement
Hc.
effectThe winner's effect estimate on the
natural scale ( applied to the stored
hr column for ratio-scale measures
OR/RR/IRR; passed through otherwise).
PconsThe winner's consistency probability.
KNumber of factors in the winner's subgroup definition.
n_passedNumber of candidates that passed
consistency (= nrow(out_sg$result)).
on_frontierLogical – did the selected
subgroup lie on the Pareto frontier? NA when the
frontier is unavailable.
errorThe error message for failed combos
(NA_character_ on success).
## Not run: cmp_cont <- compare_selection_rules(...) # continuous analysis cmp_bin <- compare_selection_rules(...) # binary analysis tidy_cont <- extract_selected_subgroups(cmp_cont, analysis_label = "continuous") tidy_bin <- extract_selected_subgroups(cmp_bin, analysis_label = "binary") all_selected <- rbind(tidy_cont, tidy_bin) saveRDS(all_selected, "selected_subgroups_all.rds") ## End(Not run)## Not run: cmp_cont <- compare_selection_rules(...) # continuous analysis cmp_bin <- compare_selection_rules(...) # binary analysis tidy_cont <- extract_selected_subgroups(cmp_cont, analysis_label = "continuous") tidy_bin <- extract_selected_subgroups(cmp_bin, analysis_label = "binary") all_selected <- rbind(tidy_cont, tidy_bin) saveRDS(all_selected, "selected_subgroups_all.rds") ## End(Not run)
Formats the figure note from plot_sg_weighted_km() output for use in Quarto or RMarkdown documents.
figure_note( x, prefix = "*Note*: ", include_definition = TRUE, include_hr_explanation = TRUE, custom_text = NULL )figure_note( x, prefix = "*Note*: ", include_definition = TRUE, include_hr_explanation = TRUE, custom_text = NULL )
x |
Output from plot_sg_weighted_km() |
prefix |
Character. Prefix for the note. Default uses italic Note. |
include_definition |
Logical. Include subgroup definition. Default: TRUE |
include_hr_explanation |
Logical. Include HR(bc) explanation. Default: TRUE |
custom_text |
Character. Additional custom text to append. Default: NULL |
Character string formatted as a figure note, or NULL if no content
## Not run: km_result <- plot_sg_weighted_km(fs.est = fs) cat(figure_note(km_result)) ## End(Not run)## Not run: km_result <- plot_sg_weighted_km(fs.est = fs) cat(figure_note(km_result)) ## End(Not run)
Returns elements of x that are in lassokeep.
filter_by_lassokeep(x, lassokeep)filter_by_lassokeep(x, lassokeep)
x |
Character vector. |
lassokeep |
Character vector of selected variables. |
Filtered character vector or NULL.
filter_by_lassokeep(c("age", "size", "nodes"), lassokeep = c("age", "nodes")) filter_by_lassokeep(c("pgr", "er"), lassokeep = c("age", "nodes")) # returns NULLfilter_by_lassokeep(c("age", "size", "nodes"), lassokeep = c("age", "nodes")) filter_by_lassokeep(c("pgr", "er"), lassokeep = c("age", "nodes")) # returns NULL
Simplifies the common pattern of filtering arguments from a source list to match a target function's formal parameters, then adding/overriding specific arguments.
filter_call_args(source_args, target_func, override_args = NULL)filter_call_args(source_args, target_func, override_args = NULL)
source_args |
List of all arguments (typically from |
target_func |
Function whose formals define the filter criteria. |
override_args |
List of arguments to add or override (optional). |
This function:
Extracts formal parameter names from target_func
Keeps only arguments from source_args that match those names
Adds or overrides with any override_args provided
Reduces boilerplate and improves readability across the codebase.
List of filtered arguments ready for do.call().
## Not run: # Instead of: args_FS <- names(formals(get_FSdata)) args_FS_filtered <- args_call_all[names(args_call_all) %in% args_FS] args_FS_filtered$df.analysis <- df.analysis args_FS_filtered$grf_cuts <- grf_cuts FSdata <- do.call(get_FSdata, args_FS_filtered) # You now write: FSdata <- do.call(get_FSdata, filter_call_args(args_call_all, get_FSdata, list(df.analysis = df.analysis, grf_cuts = grf_cuts))) ## End(Not run)## Not run: # Instead of: args_FS <- names(formals(get_FSdata)) args_FS_filtered <- args_call_all[names(args_call_all) %in% args_FS] args_FS_filtered$df.analysis <- df.analysis args_FS_filtered$grf_cuts <- grf_cuts FSdata <- do.call(get_FSdata, args_FS_filtered) # You now write: FSdata <- do.call(get_FSdata, filter_call_args(args_call_all, get_FSdata, list(df.analysis = df.analysis, grf_cuts = grf_cuts))) ## End(Not run)
Uses numerical root-finding to determine the interaction parameter (k_inter) that achieves a specified target hazard ratio in the harm subgroup. This is the most efficient method for single target calibration.
find_k_inter_for_target_hr( target_hr_harm, data, continuous_vars, factor_vars, outcome_var, event_var, treatment_var, subgroup_vars, subgroup_cuts, k_treat = 1, k_inter_range = c(-10, 10), tol = 0.001, tol_rel = 2.5, n_super = 5000, verbose = TRUE )find_k_inter_for_target_hr( target_hr_harm, data, continuous_vars, factor_vars, outcome_var, event_var, treatment_var, subgroup_vars, subgroup_cuts, k_treat = 1, k_inter_range = c(-10, 10), tol = 0.001, tol_rel = 2.5, n_super = 5000, verbose = TRUE )
target_hr_harm |
Numeric value specifying the target hazard ratio for the harm subgroup. Must be positive. |
data |
A data.frame containing the dataset to use for model fitting. |
continuous_vars |
Character vector of continuous variable names to be standardized and included as covariates. |
factor_vars |
Character vector of factor/categorical variable names to be converted to dummy variables. |
outcome_var |
Character string specifying the name of the outcome/time variable. |
event_var |
Character string specifying the name of the event/status variable (1 = event, 0 = censored). |
treatment_var |
Character string specifying the name of the treatment variable. |
subgroup_vars |
Character vector of variable names defining the subgroup. |
subgroup_cuts |
Named list of cutpoint specifications for subgroup variables.
See |
k_treat |
Numeric value for treatment effect modifier. Default is 1 (no modification). |
k_inter_range |
Numeric vector of length 2 specifying the search range for k_inter. Default is c(-10, 10). |
tol |
Numeric value specifying tolerance for root finding convergence. Default is 0.001. |
tol_rel |
Numeric. Maximum tolerated relative error, in percent; the
call stops if the achieved HR is not within |
n_super |
Integer specifying size of super population for hazard ratio calculation. Default is 5000. |
verbose |
Logical indicating whether to print progress information. Default is TRUE. |
This function uses the uniroot algorithm to solve the equation:
The algorithm typically converges within 5-10 iterations and achieves high precision (within the specified tolerance). If the root-finding fails, the function evaluates the boundaries and provides diagnostic information.
A list of class "k_inter_result" containing:
Numeric value of optimal k_inter parameter
Numeric value of achieved hazard ratio in harm subgroup
Numeric value of target hazard ratio (for reference)
Numeric value of absolute error between achieved and target HR
Object of class "aft_dgm_flex" containing the final DGM
Integer number of iterations to convergence
Character string "root-finding" indicating method used
sensitivity_analysis_k_inter for sensitivity analysis
generate_aft_dgm_flex for DGM generation
## Not run: gbsg <- survival::gbsg # Find k_inter for target HR = 2.0 in harm subgroup result <- find_k_inter_for_target_hr( target_hr_harm = 2.0, data = gbsg, continuous_vars = c("age", "er", "pgr"), factor_vars = c("meno", "grade"), outcome_var = "rfstime", event_var = "status", treatment_var = "hormon", subgroup_vars = c("er", "meno"), subgroup_cuts = list( er = list(type = "quantile", value = 0.25), meno = 0 ), k_treat = 1.0, verbose = TRUE ) cat("Optimal k_inter:", result$k_inter, "\n") cat("Achieved HR:", result$achieved_hr_harm, "\n") ## End(Not run)## Not run: gbsg <- survival::gbsg # Find k_inter for target HR = 2.0 in harm subgroup result <- find_k_inter_for_target_hr( target_hr_harm = 2.0, data = gbsg, continuous_vars = c("age", "er", "pgr"), factor_vars = c("meno", "grade"), outcome_var = "rfstime", event_var = "status", treatment_var = "hormon", subgroup_vars = c("er", "meno"), subgroup_cuts = list( er = list(type = "quantile", value = 0.25), meno = 0 ), k_treat = 1.0, verbose = TRUE ) cat("Optimal k_inter:", result$k_inter, "\n") cat("Achieved HR:", result$achieved_hr_harm, "\n") ## End(Not run)
Deprecated.
Returns only the single split immediately above leaf_node, rendered as
"var <= value". This is not a correct subgroup definition for a leaf
below depth 1: it drops the rest of the root-to-leaf path and mis-renders
right-turns (which are >, not <=). Use the path-based
.grf_build_subgroup_definition() instead, which
grf.subg.harm.survival() now calls internally. Retained (exported) only
for backward compatibility; it is no longer used inside the package and
will be removed in a future release.
find_leaf_split(tree, leaf_node)find_leaf_split(tree, leaf_node)
tree |
Policy tree object |
leaf_node |
Integer. Leaf node identifier |
Character string with a single split expression, or NULL.
grf.subg.harm.survival for the standard entry point.
## Not run: # Deprecated; grf.subg.harm.survival() builds the full path internally. ## End(Not run)## Not run: # Deprecated; grf.subg.harm.survival() builds the full path internally. ## End(Not run)
Determines the minimum subgroup sample size needed to achieve a target detection probability for a given true hazard ratio.
find_required_sample_size( theta, target_power = 0.8, prop_cens = 0.3, hr_threshold = 1.25, hr_consistency = 1, n_range = c(20L, 500L), tol = 1, verbose = TRUE )find_required_sample_size( theta, target_power = 0.8, prop_cens = 0.3, hr_threshold = 1.25, hr_consistency = 1, n_range = c(20L, 500L), tol = 1, verbose = TRUE )
theta |
Numeric. True hazard ratio in subgroup. |
target_power |
Numeric. Target detection probability (0-1). Default: 0.80 |
prop_cens |
Numeric. Proportion censored. Default: 0.3 |
hr_threshold |
Numeric. HR threshold. Default: 1.25 |
hr_consistency |
Numeric. HR consistency threshold. Default: 1.0 |
n_range |
Integer vector of length 2. Range of sample sizes to search. Default: c(20, 500) |
tol |
Numeric. Tolerance for bisection search. Default: 1 |
verbose |
Logical. Print progress. Default: TRUE |
A list with:
n_sg_required |
Minimum sample size (rounded up) |
achieved_power |
Actual detection probability at n_sg_required |
theta |
Input hazard ratio |
target_power |
Input target power |
## Not run: # Find sample size for 80% power to detect HR = 1.5 result <- find_required_sample_size( theta = 1.5, target_power = 0.80, prop_cens = 0.2 ) print(result) ## End(Not run)## Not run: # Find sample size for 80% power to detect HR = 1.5 result <- find_required_sample_size( theta = 1.5, target_power = 0.80, prop_cens = 0.2 ) print(result) ## End(Not run)
Wrapper function to fit GRF causal survival forest with appropriate settings
fit_causal_forest(X, Y, W, D, tau.rmst, RCT, seedit, tune_grf = FALSE)fit_causal_forest(X, Y, W, D, tau.rmst, RCT, seedit, tune_grf = FALSE)
X |
Matrix. Covariate matrix |
Y |
Numeric vector. Outcome variable |
W |
Numeric vector. Treatment indicator |
D |
Numeric vector. Event indicator |
tau.rmst |
Numeric. Time horizon for RMST |
RCT |
Logical. Is this RCT data? |
seedit |
Integer. Random seed |
tune_grf |
Logical. If TRUE, enables cross-validated hyperparameter
tuning via |
Causal survival forest object
library(survival) library(grf) df <- survival::gbsg X <- as.matrix(df[, c("age", "meno", "size", "nodes", "pgr", "er")]) tau <- quantile(df$rfstime[df$status == 1], 0.6) cs <- fit_causal_forest(X = X, Y = df$rfstime, W = df$hormon, D = df$status, tau.rmst = tau, RCT = FALSE, seedit = 42L)library(survival) library(grf) df <- survival::gbsg X <- as.matrix(df[, c("age", "meno", "size", "nodes", "pgr", "er")]) tau <- quantile(df$rfstime[df$status == 1], 0.6) cs <- fit_causal_forest(X = X, Y = df$rfstime, W = df$hormon, D = df$status, tau.rmst = tau, RCT = FALSE, seedit = 42L)
Fits Cox models for two subgroups defined by treatment recommendation.
fit_cox_models(df, formula, treat.name = NULL)fit_cox_models(df, formula, treat.name = NULL)
df |
Data frame. |
formula |
Cox model formula. |
treat.name |
Character or |
List with HR and SE for each subgroup.
## Not run: library(survival) df <- data.frame( tte = gbsg$rfstime / 30.4375, event = gbsg$status, treat = gbsg$hormon, treat.recommend = as.integer(gbsg$er > 0) ) formula <- build_cox_formula("tte", "event", "treat") fit_cox_models(df, formula) ## End(Not run)## Not run: library(survival) df <- data.frame( tte = gbsg$rfstime / 30.4375, event = gbsg$status, treat = gbsg$hormon, treat.recommend = as.integer(gbsg$er > 0) ) formula <- build_cox_formula("tte", "event", "treat") fit_cox_models(df, formula) ## End(Not run)
Fits policy trees of depths 1 through maxdepth and computes metrics
fit_policy_trees(X, data, dr.scores, maxdepth, n.min)fit_policy_trees(X, data, dr.scores, maxdepth, n.min)
X |
Matrix. Covariate matrix |
data |
Data frame. Original data |
dr.scores |
Matrix. Doubly robust scores |
maxdepth |
Integer. Maximum tree depth (1-3) |
n.min |
Integer. Minimum subgroup size |
List with trees and combined values
## Not run: # fit_policy_trees() is called internally by grf.subg.harm.survival(). # See grf.subg.harm.survival() for the standard entry point. ## End(Not run)## Not run: # fit_policy_trees() is called internally by grf.subg.harm.survival(). # See grf.subg.harm.survival() for the standard entry point. ## End(Not run)
Converts a panel's row count to the fig.height (inches) used by the
vignette chunks: rows * row_height + overhead, rounded to 0.1.
forest_height( x, panel = c("single", "combo", "highrisk"), row_height = 0.45, overhead = 1.5 )forest_height( x, panel = c("single", "combo", "highrisk"), row_height = 0.45, overhead = 1.5 )
x |
A |
panel |
|
row_height |
Inches per row (default 0.45). |
overhead |
Fixed inches for title, x-axis, and footnote (default 1.5). |
A single numeric, rounded to one decimal.
## Not run: fh_single <- forest_height(S, "single") ## End(Not run)## Not run: fh_single <- forest_height(S, "single") ## End(Not run)
Implements advanced statistical methods for exploratory subgroup
identification in clinical trials. Provides tools for identifying
patient subgroups with differential treatment effects using machine
learning approaches including Generalized Random Forests ('GRF'),
LASSO regularization, and exhaustive combinatorial search algorithms.
Supports survival endpoints (Cox proportional hazards), binary
outcomes (log odds ratio, log relative risk, risk difference),
continuous outcomes (mean difference), and count / rate outcomes
(log incidence rate ratio via Poisson, quasi-Poisson, or
negative-binomial GLMs with optional person-time offset). Features
bootstrap bias correction using infinitesimal jackknife methods to
address selection bias in post-hoc analyses. Designed for clinical
researchers conducting exploratory subgroup analyses in randomized
controlled trials, particularly for multi-regional clinical trials
('MRCT') requiring regional consistency evaluation. Methods are
described in Leon et al. (2024) doi:10.1002/sim.10163.
Post-selection inference for the reported subgroup (multiplier
resampling, mr_inference = TRUE) follows León and Anderson
(2026a) doi:10.48550/arXiv.2609.38361. The current methodology for
the search itself – forest search as closed-form stability selection
on splitting consistency, the family-wise false-declaration rate and
the two-subgroup claim – is given in León and Anderson (2026b)
doi:10.48550/arXiv.2610.28379.
forestsearch( df.analysis, outcome.name = "tte", event.name = "event", treat.name = "treat", id.name = "id", potentialOutcome.name = NULL, flag_harm.name = NULL, confounders.name = NULL, parallel_args = list(plan = "multisession", workers = .default_parallel_workers(), show_message = TRUE), df.predict = NULL, df.test = NULL, is.RCT = TRUE, seedit = 8316951, est.scale = "hr", use_lasso = FALSE, use_grf = FALSE, grf_res = NULL, grf_cuts = NULL, use_dina = FALSE, dina_res = NULL, dina_cuts = NULL, dina_args = list(), dina_select_statistic = c("effect", "dina"), subgroup_method = c("consistency", "dina", "grf"), max_n_confounders = 1000, grf_depth = 2, grf_selection = c("frontier", "tree"), grf_select_statistic = c("effect", "dr"), dmin.grf = 0, frac.tau = 0.8, return_selected_cuts_only = TRUE, conf_force = NULL, defaultcut_names = NULL, cut_type = "default", exclude_cuts = NULL, replace_med_grf = FALSE, cont.cutoff = 4, conf.cont_medians = NULL, conf.cont_medians_force = NULL, conf.cont_jcuts = NULL, collapse_cuts = TRUE, collapse_cuts_args = list(), n.min = 60, n.min.frac = 0.1, effect.threshold = NULL, consistency.threshold = NULL, hr.threshold = 1.25, hr.consistency = 1, sg_focus = "hr", selection_rule = "neighborhood", effect_neighborhood = 0.1, fs.splits = 1000, m1.threshold = Inf, pconsistency.threshold = 0.9, stop_threshold = pconsistency.threshold, pconsistency.digits = 2, show_candidate_summary = FALSE, max_print = 10, d0.min = 10, d1.min = 10, max.minutes = 3, minp = 0.025, details = FALSE, quiet = FALSE, maxk = 2, by.risk = 12, plot.sg = FALSE, plot.grf = FALSE, max_subgroups_search = Inf, vi.grf.min = NULL, use_twostage = TRUE, twostage_args = list(), consistency_method = c("resample", "split"), outcome_type = c("survival", "binary", "continuous", "count"), effect_measure = NULL, offset.name = NULL, adverse_outcome = NULL, overdispersion = c("none", "quasi", "negbin"), grf_count_transform = c("log", "identity"), tune_grf = FALSE, adjust_covariates = NULL, ps_method = NULL, ps_adjust_method = c("none", "iptw", "dr_gcomp"), ps_hat = NULL, mr_inference = FALSE, mr_inference_args = list() )forestsearch( df.analysis, outcome.name = "tte", event.name = "event", treat.name = "treat", id.name = "id", potentialOutcome.name = NULL, flag_harm.name = NULL, confounders.name = NULL, parallel_args = list(plan = "multisession", workers = .default_parallel_workers(), show_message = TRUE), df.predict = NULL, df.test = NULL, is.RCT = TRUE, seedit = 8316951, est.scale = "hr", use_lasso = FALSE, use_grf = FALSE, grf_res = NULL, grf_cuts = NULL, use_dina = FALSE, dina_res = NULL, dina_cuts = NULL, dina_args = list(), dina_select_statistic = c("effect", "dina"), subgroup_method = c("consistency", "dina", "grf"), max_n_confounders = 1000, grf_depth = 2, grf_selection = c("frontier", "tree"), grf_select_statistic = c("effect", "dr"), dmin.grf = 0, frac.tau = 0.8, return_selected_cuts_only = TRUE, conf_force = NULL, defaultcut_names = NULL, cut_type = "default", exclude_cuts = NULL, replace_med_grf = FALSE, cont.cutoff = 4, conf.cont_medians = NULL, conf.cont_medians_force = NULL, conf.cont_jcuts = NULL, collapse_cuts = TRUE, collapse_cuts_args = list(), n.min = 60, n.min.frac = 0.1, effect.threshold = NULL, consistency.threshold = NULL, hr.threshold = 1.25, hr.consistency = 1, sg_focus = "hr", selection_rule = "neighborhood", effect_neighborhood = 0.1, fs.splits = 1000, m1.threshold = Inf, pconsistency.threshold = 0.9, stop_threshold = pconsistency.threshold, pconsistency.digits = 2, show_candidate_summary = FALSE, max_print = 10, d0.min = 10, d1.min = 10, max.minutes = 3, minp = 0.025, details = FALSE, quiet = FALSE, maxk = 2, by.risk = 12, plot.sg = FALSE, plot.grf = FALSE, max_subgroups_search = Inf, vi.grf.min = NULL, use_twostage = TRUE, twostage_args = list(), consistency_method = c("resample", "split"), outcome_type = c("survival", "binary", "continuous", "count"), effect_measure = NULL, offset.name = NULL, adverse_outcome = NULL, overdispersion = c("none", "quasi", "negbin"), grf_count_transform = c("log", "identity"), tune_grf = FALSE, adjust_covariates = NULL, ps_method = NULL, ps_adjust_method = c("none", "iptw", "dr_gcomp"), ps_hat = NULL, mr_inference = FALSE, mr_inference_args = list() )
df.analysis |
Data frame. Analysis dataset with required columns. |
||||||||||||||||||||||||
outcome.name |
Character. Name of time-to-event outcome variable. Default "tte". |
||||||||||||||||||||||||
event.name |
Character. Name of event indicator (1=event, 0=censored). Default "event". |
||||||||||||||||||||||||
treat.name |
Character. Name of treatment variable (1=treatment, 0=control). Default "treat". |
||||||||||||||||||||||||
id.name |
Character. Name of subject ID variable. Default "id". |
||||||||||||||||||||||||
potentialOutcome.name |
Character. Name of potential outcome variable (optional). |
||||||||||||||||||||||||
flag_harm.name |
Character. Name of true harm flag for simulations (optional). |
||||||||||||||||||||||||
confounders.name |
Character vector. Names of candidate subgroup-defining variables. |
||||||||||||||||||||||||
parallel_args |
List. Parallel processing configuration:
To disable parallelism entirely, pass
|
||||||||||||||||||||||||
df.predict |
Data frame. Prediction dataset (optional). |
||||||||||||||||||||||||
df.test |
Data frame. Test dataset (optional). |
||||||||||||||||||||||||
is.RCT |
Logical. Is this a randomized controlled trial? Default TRUE. |
||||||||||||||||||||||||
seedit |
Integer. Random seed. Default 8316951. |
||||||||||||||||||||||||
est.scale |
Character. Estimation scale ("hr" or "rmst"). Default "hr". |
||||||||||||||||||||||||
use_lasso |
Logical. Use LASSO for variable selection. Default FALSE.
The default changed from |
||||||||||||||||||||||||
use_grf |
Logical. Generate additional candidate cuts from a GRF
fit: tree-derived cutpoints are extracted via
|
||||||||||||||||||||||||
grf_res |
GRF results object (optional, for reuse). |
||||||||||||||||||||||||
grf_cuts |
List. Custom GRF cut points (optional). |
||||||||||||||||||||||||
use_dina |
Logical. Generate additional screening-stage candidate
cuts from a DINA per-covariate Pareto frontier, fed into
|
||||||||||||||||||||||||
dina_res |
A fitted DINA object (optional, for reuse). When NULL
and |
||||||||||||||||||||||||
dina_cuts |
Character vector of pre-supplied DINA cut expressions (optional). When supplied, the DINA fit / frontier step is skipped and these are used directly. |
||||||||||||||||||||||||
dina_args |
Named list of DINA tuning options, all optional.
Recognised keys:
The same holds for every frontier key, not only
Under |
||||||||||||||||||||||||
dina_select_statistic |
Character, one of |
||||||||||||||||||||||||
subgroup_method |
Character, one of |
||||||||||||||||||||||||
max_n_confounders |
Deprecated (0.4.0.9000): accepted and
ignored with a warning. GRF variable-importance screening of the
candidate cut columns was removed so that the candidate family is the
cut grid alone unless |
||||||||||||||||||||||||
grf_depth |
Integer. GRF tree depth. Default 2. |
||||||||||||||||||||||||
grf_selection |
Character, one of |
||||||||||||||||||||||||
grf_select_statistic |
Character, one of |
||||||||||||||||||||||||
dmin.grf |
Numeric. Minimum events for GRF. Default 0.0. |
||||||||||||||||||||||||
frac.tau |
Numeric in (0, 1]. Multiplier on the GRF time horizon
passed to |
||||||||||||||||||||||||
return_selected_cuts_only |
Logical. If TRUE (default), GRF returns only cuts from the
tree depth that identified the selected subgroup meeting |
||||||||||||||||||||||||
conf_force |
Character vector of forced cut expressions (optional),
e.g. |
||||||||||||||||||||||||
defaultcut_names |
Character vector. Default cut variable names (optional). |
||||||||||||||||||||||||
cut_type |
Character. Cut strategy: |
||||||||||||||||||||||||
exclude_cuts |
Character vector. Variables to exclude from cutting (optional). |
||||||||||||||||||||||||
replace_med_grf |
Logical. Replace median with GRF cuts. Default FALSE. |
||||||||||||||||||||||||
cont.cutoff |
Integer. Cutoff for continuous vs categorical. Default 4. |
||||||||||||||||||||||||
conf.cont_medians |
Character vector of continuous confounders to cut at
their median (optional). Names, not values – the cut location is the
median computed from the supplied data, so it is not fixed by this
argument and moves with a resample. To pin a cut location, pass a literal
cut expression via |
||||||||||||||||||||||||
conf.cont_medians_force |
Character vector of additional continuous
confounders to force a median cut on (optional). Names, not values, with
the same consequence as |
||||||||||||||||||||||||
conf.cont_jcuts |
Named integer list (each value >= 1). Per-variable
J-quantile cut override for continuous covariates: for an entry
|
||||||||||||||||||||||||
collapse_cuts |
Logical. If TRUE, collapse near-redundant continuous
candidate cuts before the search. Continuous covariates can generate many
thresholds that are practically redundant (e.g. |
||||||||||||||||||||||||
collapse_cuts_args |
List of overrides merged onto the defaults
|
||||||||||||||||||||||||
n.min |
Integer or |
||||||||||||||||||||||||
n.min.frac |
Numeric in (0, 1). Fraction of the analysis sample size
used for the adaptive |
||||||||||||||||||||||||
effect.threshold |
Numeric or NULL. |
||||||||||||||||||||||||
consistency.threshold |
Numeric or NULL. |
||||||||||||||||||||||||
hr.threshold |
Numeric. Legacy name for |
||||||||||||||||||||||||
hr.consistency |
Numeric. Legacy name for
|
||||||||||||||||||||||||
sg_focus |
Character. Subgroup selection focus – what is
selected, not merely how candidates are sorted. Except for
The descriptions above are for For Per-engine resolution. Every accepted spelling, and the rule that actually runs for it on each engine:
The collapse sets, stated explicitly. On
Default Scope of the band arguments. Choosing between the three effect-oriented rules.
|
||||||||||||||||||||||||
selection_rule |
Character. Rule defining the candidate
inclusion set for |
||||||||||||||||||||||||
effect_neighborhood |
Numeric in |
||||||||||||||||||||||||
fs.splits |
Integer. Number of splits for consistency evaluation (or maximum
splits when |
||||||||||||||||||||||||
m1.threshold |
Numeric. Maximum median survival threshold. Default Inf. |
||||||||||||||||||||||||
pconsistency.threshold |
Numeric. |
||||||||||||||||||||||||
stop_threshold |
Numeric in Applies to For every other focus
Note: Values > 1.0 are not permitted. To disable early
stopping, use |
||||||||||||||||||||||||
pconsistency.digits |
Integer. Number of decimal places to which the
consistency proportion is rounded before it is compared with
|
||||||||||||||||||||||||
show_candidate_summary |
Logical. If |
||||||||||||||||||||||||
max_print |
Integer. Maximum number of candidate subgroups to print in
the pre- and post-consistency candidate summaries when
|
||||||||||||||||||||||||
d0.min |
Integer. Minimum per-arm filter for candidate subgroups.
For |
||||||||||||||||||||||||
d1.min |
Integer. Same as |
||||||||||||||||||||||||
max.minutes |
Deprecated (0.4.0.9000): accepted and
ignored. It was documented as a wall-clock budget for the
combination search but was never enforced in the parallel search
path. A non-default value emits one warning. Search scope is governed
by |
||||||||||||||||||||||||
minp |
Numeric. Minimum prevalence threshold. Default 0.025. |
||||||||||||||||||||||||
details |
Logical. Print progress details. Default FALSE. |
||||||||||||||||||||||||
quiet |
Logical. If TRUE, suppress the configuration summary
message printed at startup. Useful when |
||||||||||||||||||||||||
maxk |
Integer. Maximum number of factors per subgroup. Default 2. |
||||||||||||||||||||||||
by.risk |
Integer. Risk table interval. Default 12. |
||||||||||||||||||||||||
plot.sg |
Logical. Plot subgroup survival curves. Default FALSE. |
||||||||||||||||||||||||
plot.grf |
Logical. Plot GRF results. Default FALSE. |
||||||||||||||||||||||||
max_subgroups_search |
Numeric. Maximum subgroups to evaluate.
Default Set a finite value only to bound runtime, knowing it can change which
subgroup is selected. The default was |
||||||||||||||||||||||||
vi.grf.min |
Deprecated (0.4.0.9000): accepted and ignored
with a warning. GRF variable-importance screening of the candidate cut
columns was removed so that the candidate family is the cut grid alone
unless |
||||||||||||||||||||||||
use_twostage |
Logical. Use two-stage sequential consistency algorithm for
improved performance. Default FALSE for backward compatibility. When TRUE,
|
||||||||||||||||||||||||
twostage_args |
List. Parameters for two-stage algorithm (only used when
|
||||||||||||||||||||||||
consistency_method |
Character. |
||||||||||||||||||||||||
outcome_type |
Character. One of |
||||||||||||||||||||||||
effect_measure |
Character or |
||||||||||||||||||||||||
offset.name |
Character or |
||||||||||||||||||||||||
adverse_outcome |
Logical or |
||||||||||||||||||||||||
overdispersion |
Character. Overdispersion correction for
|
||||||||||||||||||||||||
grf_count_transform |
Character. Transformation applied to the
count outcome before passing to |
||||||||||||||||||||||||
tune_grf |
Logical. If |
||||||||||||||||||||||||
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
||||||||||||||||||||||||
ps_method |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character or GLM outcome types only ( |
||||||||||||||||||||||||
ps_adjust_method |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character. PS adjustment method: GLM outcome types only, and consumed only when |
||||||||||||||||||||||||
ps_hat |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Numeric vector or Not carried into bootstrap replicates or cross-validation folds: a
resample has the same row count but different subjects, so a supplied
score would be attached to the wrong people. Both re-estimate their own
score per replicate/fold under |
||||||||||||||||||||||||
mr_inference |
Logical. Whether to follow the search with multiplier
resampling (MR): a refit-free de-biased estimate of the selected
subgroup's treatment effect, an interval for it, and a harm-confirmation
flag. Default |
||||||||||||||||||||||||
mr_inference_args |
List of optional MR controls; used only when
|
Algorithm Overview:
Variable Selection: GRF identifies variables with treatment effect heterogeneity; LASSO selects most predictive
Subgroup Discovery: Exhaustive search over factor combinations
up to maxk
Consistency Validation: Split-sample validation ensures reproducibility
Selection: Choose subgroup based on sg_focus criterion
Two-Stage Consistency Algorithm:
When use_twostage = TRUE, the consistency evaluation uses an optimized
algorithm that can provide 3-10x speedup:
Stage 1: Quick screening with n.splits.screen splits
eliminates clearly non-viable candidates
Stage 2: Sequential batched evaluation with early stopping for candidates passing Stage 1
The two-stage algorithm is recommended for:
Exploratory analyses with many candidate subgroups
Large fs.splits values (>200)
Iterative model development
For final regulatory submissions, use_twostage = FALSE may be preferred
for exact reproducibility.
A list of class "forestsearch" containing:
Consistency evaluation results including:
out_sg: Selected subgroup based on sg_focus
sg_focus: Focus criterion used
df_flag: Treatment recommendations
sg.harm: Subgroup definition labels (character vector
of cut names) – same as the top-level sg.harm
sg.harm.id: per-subject 0/1 membership indicator (not a character vector of cut expressions; see the Field naming collision section below)
algorithm: "twostage" or "fixed"
n_candidates_evaluated: Number evaluated
n_passed: Number passing threshold
Subgroup search results
Candidate confounders considered
Confounders after variable selection
Analysis data with treatment recommendations
Prediction data with recommendations (if provided)
Test data with recommendations (if provided)
Total computation time
GRF results object
Subgroup focus criterion used
Selected subgroup definition – character vector
of factor-level cut names. Recommended accessor for
displaying the identified subgroup (e.g.,
paste(obj$sg.harm, collapse = " & ")). NULL if
no subgroup was identified.
GRF cut points used
Proportion of max combinations searched
Maximum subgroup HR estimate
GRF plot object (if plot.grf = TRUE)
All arguments for reproducibility
The resolved threshold configuration:
outcome_type, effect_measure, screening,
consistency, screening_natural,
consistency_natural, scale, pconsistency and
two description strings. Read $scale before reading
$screening. The phrase "the screening threshold" names two
different numbers depending on which object is inspected:
On the survival path $screening is
log(hr.threshold) – the scale the admission set and
multiplier resampling work on – while the candidate search
itself compares the fitted hazard ratio against
hr.threshold on the natural scale. The two
are the same threshold on two scales, and
$screening_natural holds the search's value.
On the GLM paths $screening is the value
the search compares against directly: the link scale for ratio
measures (log of the supplied ratio) and the identity
scale for RD, IRD and MD.
$consistency follows the same convention, and
$pconsistency is the rate pconsistency.threshold,
which has no scale.
Character scalar recording whether the candidate
family the identifier ranked over is fixed – the condition
multiplier resampling requires (see mr_inference).
"no-front-end": subgroup_method = "consistency" with
use_lasso, use_grf and use_dina all
FALSE. Named for what it checks: no fitted model shapes the
family on the observed data. This is weaker than the manuscript's
Section 2.1 fixed family, which additionally requires resample-
invariant cut locations – quantile-derived cuts are not. "conditional-removable": the same method with
at least one front end on – data-dependent, but fixed by turning
those off. "conditional-inherent": subgroup_method of
"dina" or "grf", where the family is generated by
fitting a model to the same data and cannot be made fixed.
Descriptive only; it never raises a condition.
Multiplier-resampling (MR) result, or NULL
when MR did not run. See fs_mr_inference for fields
(naive, debiased, selection_bias,
settings, harm_flag, timing_seconds). MR
approximates the full bootstrap (FB) to leading order; its interval is
the infinitesimal-jackknife analogue of the FB interval under the
default ci_method = "field" and under "ij", and the
subgroup robust-SE interval under "wald". Under the default
the return also carries the field element.
Logical, three-valued. TRUE when MR ran
and the de-biased estimate still indicates harm under
confirm_rule; FALSE when MR ran and it does not;
NA when MR did not run or could not be computed.
NA is therefore absence of evidence, not evidence
against harm – test it with is.na() before
isTRUE(), which collapses NA to FALSE and so
reports "harm not confirmed" for an analysis that never ran. See
the vocabulary section.
Three arguments carry thresholds, and two of them have the word "consistency" in the name. They are different quantities:
c1 – effect.threshold (legacy hr.threshold)The screening threshold on a candidate subgroup's own effect, estimated on the full sample. A candidate must clear it to enter the consistency stage.
c2 – consistency.threshold (legacy
hr.consistency)An effect threshold, applied to the subgroup's effect estimated within each split half. Not a proportion, and not the consistency rate.
p* – pconsistency.threshold
A proportion in
[0, 1]: the fraction of splits that must clear c2.
Not an effect threshold.
In one line: c2 sets the bar a split half must clear, and p* counts how
often that bar is met.
Resolved defaults, per estimand. Reading
hr.consistency's default as "1.0" is misleading on the GLM paths:
when neither threshold is supplied, the value a run actually uses is
resolved from the estimand. The resolved pairs are:
| resolved estimand | c1 | c2 |
HR (survival) |
1.25, compared by the search on the natural HR scale | 1.0 |
OR, RR, IRR |
log(1.25) | log(1.0), i.e. 0 |
RD |
0.05 | 0.0 |
IRD |
0.01 | 0.0 |
MD |
0.0 | 0.0 |
Supplying effect.threshold or consistency.threshold (or the
legacy names) suppresses the corresponding remap and the supplied value is
used on the scale shown above.
The pair rule, on the consistency path with a ratio estimand. For
subgroup_method = "consistency" with survival (HR) or binary
effect_measure = "OR" – the two estimands whose c1 and c2 sit on
one comparable ratio scale – the two thresholds are treated as a pair:
c2 > c1 is an error. It is degenerate: the
consistency-stage entry condition re-screens the admitted family on c2,
so candidates the screen let through are discarded at the stage boundary,
and when none survives the consistency stage does not run at all and the
fit reads as a finding of no subgroup. c2 = c1 is allowed.
c1 supplied without c2 derives
c2 = 0.80 * c1 on the ratio scale (on the log scale an
additive shift of log(0.80), never 0.80 * log(c1)):
1.25 -> 1.00, 1.00 -> 0.80, 0.90 -> 0.72. Announced with a message, and
carried into every bootstrap replicate and CV fold. The default pair
(1.25, 1.0) is the rule's fixed point, so a call that sets neither
threshold is unaffected. An explicitly supplied c2, in either
spelling, is never overridden.
Supplying both spellings of one threshold at disagreeing values is an error naming both.
Neither rule applies under subgroup_method = "dina" or
"grf" (which return before the consistency stage and never consult
c2), nor to the identity-scale estimands RD, IRD,
MD, nor to IRR.
pconsistency.threshold is not remapped. It is a rate, and
it is identical for every outcome_type and every estimand.
The top-level sg.harm on this object and the
grp.consistency$sg.harm nested field both hold a character
vector of cut expressions. The nested grp.consistency$sg.harm.id
field holds a per-subject 0/1 membership indicator, not a cut
vector – unlike sg.harm.id on GRF result objects, which
does hold cut expressions. See
subgroup.consistency and grf.subg.harm.glm
for the full discussion of this naming collision. Code that must
handle both object types should prefer the top-level sg.harm
accessor on forestsearch results.
Four different things in this package used to share the word "gate". They are distinct, they happen at different stages, and only the last one is a pass/fail decision:
Which candidate subgroups are even eligible
to be considered: hr.threshold, pconsistency.threshold,
n.min, d0.min, d1.min, maxk. Applied during
the search.
Which admissible candidate is chosen as the
estimated subgroup: sg_focus. Applied during the search.
Correcting the chosen subgroup's treatment effect for the optimism of having chosen it, and putting an interval around the corrected value. This is FB and MR below. Applied after the search.
The one pass/fail: does the corrected estimate
still indicate harm? This is confirm_rule against
t_confirm, reported as mr_harm_confirmed.
Full bootstrap (FB) — forestsearch_bootstrap_dofuture.
Resamples subjects, and re-runs the entire subgroup search inside
every replicate, so the replicate may select a different subgroup than the
original analysis did. That is what lets it correct the selection-induced
bias in the reported treatment effect, and supply an
infinitesimal-jackknife interval. It is the reference method, and it is
expensive: cost scales with nb_boots times the cost of one search.
Multiplier resampling (MR) — fs_mr_inference, turned
on by mr_inference = TRUE. Resamples nothing; it perturbs the
per-subject influence contributions (dfbeta) of a single set of
fits with random mean-zero multipliers, and re-applies the selection rule on
each perturbed draw. It corrects the same selection bias FB corrects and
produces the same kind of interval, to leading order, at a small fraction of
the cost — no refits. MR is post-selection inference on a
completed analysis: it runs after the subgroup has been chosen and
cannot change which subgroup is identified. Running with
mr_inference = TRUE and FALSE gives identical
sg.harm, df.est and max_sg_est. MR approximates FB;
it does not replace it.
Harm confirmation. Once MR has produced a de-biased estimate,
mr_inference_args$confirm_rule decides whether that estimate still
indicates harm:
"point" (default)Compares the de-biased point
estimate to t_confirm.
"ci"Compares the one-sided 95% selection-adjusted
lower bound to t_confirm. Strictly the stronger
requirement, so anything it confirms "point" also confirms.
t_confirm is the threshold those rules compare against, on
the effect scale (HR, OR, RR, IRR; or RD, MD for differences) —
not the working log scale used internally. Left NULL it resolves to
the null effect: 1 for ratio measures, 0 for differences. It
is deliberately set near the null rather than at hr.threshold,
because the de-biasing correction over-shrinks genuine effects, so
re-applying the screening threshold to a corrected estimate would discard
true findings.
mr_harm_confirmed carries the answer, and is three-valued:
TRUE = MR ran and harm is confirmed; FALSE = MR ran and harm
is not confirmed; NA = MR did not run, or could not be computed.
NA is not evidence against harm. It arises when
mr_inference = FALSE (the default), when no subgroup was identified,
or when the MR step was skipped or failed for the outcome/consistency
combination in use. Because isTRUE(NA) is FALSE, code that
reads this field with isTRUE() alone silently reports "harm not
confirmed" for an analysis that was never performed; branch on
is.na() first.
The arguments adjust_covariates, ps_method,
ps_adjust_method and ps_hat are implemented, but the
published analyses and simulation studies of León and Anderson (2026a,
2026b) use the unadjusted treatment-only fits, and no coverage or bias
results exist for these paths. They are untested, not known to be wrong.
Users who need them should check them on their own data, for example
against a direct coxph() or glm() fit of the selected
subgroup.
León, L. F. and Anderson, K. M. (2026a). Inference for standard trial estimands after data-driven subgroup discovery. arXiv:2609.38361. doi:10.48550/arXiv.2609.38361
León, L. F. and Anderson, K. M. (2026b). Interpretable subgroup identification across survival and generalized-linear outcomes. arXiv:2610.28379. doi:10.48550/arXiv.2610.28379
FDA Guidance for Industry: Enrichment Strategies for Clinical Trials
Athey & Imbens (2016). Recursive partitioning for heterogeneous causal effects. PNAS.
Wager & Athey (2018). Estimation and inference of heterogeneous treatment effects using random forests. JASA.
León et al. (2024). doi:10.1002/sim.10163
subgroup.consistency for consistency evaluation details
forestsearch_bootstrap_dofuture for the full bootstrap (FB)
fs_mr_inference for multiplier resampling (MR), the
mr_inference = TRUE step
mr_estimates_table to render the MR estimates
fs_fdr_report to sweep harm-confirmation thresholds
(c_confirm) and measure what confirmation buys
forestsearch_Kfold for cross-validation
Package website: https://larry-leon.github.io/forestsearch/
Source code: https://github.com/larry-leon/forestsearch
## Not run: ## Example 1 -- Survival (GBSG, hormone therapy): the analysis of ## León and Anderson (2026a, Sec. 5) and of the survival-post-selection ## article on the package website. Forest search over the cut grid (no ## model-based front ends), effMaxSG selection in a 10% effect ## neighbourhood, multiplier-resampling inference for the reported ## subgroup computed inside the fit. library(survival) df_gbsg <- within(gbsg, { id <- seq_len(nrow(gbsg)) time_months <- rfstime / 30.4375 grade3 <- ifelse(grade == "3", 1, 0) }) fs_surv <- forestsearch( df.analysis = df_gbsg, outcome.name = "time_months", event.name = "status", treat.name = "hormon", id.name = "id", confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), is.RCT = TRUE, use_lasso = FALSE, use_grf = FALSE, use_dina = FALSE, conf.cont_jcuts = list(er = 10, pgr = 10), conf_force = c("er <= 0", "pgr <= 0"), subgroup_method = "consistency", sg_focus = "effMaxSG", selection_rule = "neighborhood", effect_neighborhood = 0.10, hr.threshold = 1.0, hr.consistency = 1.0, pconsistency.threshold = 0.90, consistency_method = "resample", fs.splits = 1000, n.min = 60, d0.min = 10, d1.min = 10, maxk = 2, mr_inference = TRUE, mr_inference_args = list(draws = 5000L), parallel_args = list(plan = "multisession", workers = 4), seedit = 8316951 ) print(fs_surv) mr_estimates_table(fs_surv) ## Example 2 -- Binary outcome, odds ratio (ACTG175, ZDV+ddI vs ddI): ## the binary application of León and Anderson (2026b). Adverse-scale ## outcome (1 = no CD4 improvement at week 20), OR thresholds at the null, ## same selection rule and multiplier-resampling gate as Example 1. if (requireNamespace("speff2trial", quietly = TRUE)) { actg <- subset(speff2trial::ACTG175, arms %in% c(1L, 3L)) actg <- actg[!is.na(actg$cd420), ] actg$id <- seq_len(nrow(actg)) actg$treat <- as.integer(actg$arms == 1L) actg$y_binary_neg <- as.integer(!(actg$cd420 > actg$cd40)) bin_vars <- c("hemo", "homo", "drugs", "race", "gender", "symptom") for (v in bin_vars) actg[[v]] <- as.numeric(actg[[v]]) fs_bin <- forestsearch( df.analysis = actg, outcome.name = "y_binary_neg", event.name = "y_binary_neg", treat.name = "treat", id.name = "id", confounders.name = c("age", "preanti", "wtkg", "karnof", "cd40", "cd80", bin_vars), outcome_type = "binary", effect_measure = "OR", adverse_outcome = TRUE, use_lasso = FALSE, use_grf = FALSE, use_dina = FALSE, conf.cont_jcuts = list(cd40 = 10, wtkg = 10), subgroup_method = "consistency", sg_focus = "effMaxSG", selection_rule = "neighborhood", effect_neighborhood = 0.10, effect.threshold = 1.0, consistency.threshold = 1.0, pconsistency.threshold = 0.90, consistency_method = "resample", fs.splits = 500, n.min = 60, d0.min = 10, d1.min = 10, maxk = 2, mr_inference = TRUE, mr_inference_args = list(draws = 5000L), parallel_args = list(plan = "multisession", workers = 4), seedit = 8316951 ) print(fs_bin) } ## Example 3 -- Continuous outcome, mean difference (ACTG175): the ## continuous application of León and Anderson (2026b), with the field ## (calibrated one-sided) bounds for the selected subgroup and its ## complement. Outcome is CD4 decline from baseline to week 20 (adverse ## scale); thresholds are on the mean-difference scale. if (requireNamespace("speff2trial", quietly = TRUE)) { actg <- subset(speff2trial::ACTG175, arms %in% c(1L, 3L)) actg <- actg[!is.na(actg$cd420), ] actg$id <- seq_len(nrow(actg)) actg$treat <- as.integer(actg$arms == 1L) actg$y_decline <- actg$cd40 - actg$cd420 bin_vars <- c("hemo", "homo", "drugs", "race", "gender", "symptom") for (v in bin_vars) actg[[v]] <- as.factor(actg[[v]]) fs_cont <- forestsearch( df.analysis = actg, outcome.name = "y_decline", treat.name = "treat", id.name = "id", confounders.name = c("age", "preanti", "wtkg", "karnof", "cd40", "cd80", bin_vars), outcome_type = "continuous", effect_measure = "MD", adverse_outcome = TRUE, use_lasso = FALSE, use_grf = FALSE, use_dina = FALSE, conf.cont_jcuts = list(age = 10, preanti = 10, wtkg = 10, karnof = 10, cd40 = 10, cd80 = 10), sg_focus = "effMaxSG", selection_rule = "neighborhood", effect_neighborhood = 0.20, effect.threshold = 10, consistency.threshold = 5, pconsistency.threshold = 0.90, consistency_method = "resample", fs.splits = 500, n.min = 60, d0.min = 10, d1.min = 10, maxk = 2, mr_inference = TRUE, mr_inference_args = list(ci_method = "field", draws = 5000L, include_complement = TRUE, field_complement = TRUE, field_scale_complement = "selected", return_reselection = TRUE), parallel_args = list(plan = "sequential"), seedit = 8316951 ) print(fs_cont) } ## End(Not run)## Not run: ## Example 1 -- Survival (GBSG, hormone therapy): the analysis of ## León and Anderson (2026a, Sec. 5) and of the survival-post-selection ## article on the package website. Forest search over the cut grid (no ## model-based front ends), effMaxSG selection in a 10% effect ## neighbourhood, multiplier-resampling inference for the reported ## subgroup computed inside the fit. library(survival) df_gbsg <- within(gbsg, { id <- seq_len(nrow(gbsg)) time_months <- rfstime / 30.4375 grade3 <- ifelse(grade == "3", 1, 0) }) fs_surv <- forestsearch( df.analysis = df_gbsg, outcome.name = "time_months", event.name = "status", treat.name = "hormon", id.name = "id", confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), is.RCT = TRUE, use_lasso = FALSE, use_grf = FALSE, use_dina = FALSE, conf.cont_jcuts = list(er = 10, pgr = 10), conf_force = c("er <= 0", "pgr <= 0"), subgroup_method = "consistency", sg_focus = "effMaxSG", selection_rule = "neighborhood", effect_neighborhood = 0.10, hr.threshold = 1.0, hr.consistency = 1.0, pconsistency.threshold = 0.90, consistency_method = "resample", fs.splits = 1000, n.min = 60, d0.min = 10, d1.min = 10, maxk = 2, mr_inference = TRUE, mr_inference_args = list(draws = 5000L), parallel_args = list(plan = "multisession", workers = 4), seedit = 8316951 ) print(fs_surv) mr_estimates_table(fs_surv) ## Example 2 -- Binary outcome, odds ratio (ACTG175, ZDV+ddI vs ddI): ## the binary application of León and Anderson (2026b). Adverse-scale ## outcome (1 = no CD4 improvement at week 20), OR thresholds at the null, ## same selection rule and multiplier-resampling gate as Example 1. if (requireNamespace("speff2trial", quietly = TRUE)) { actg <- subset(speff2trial::ACTG175, arms %in% c(1L, 3L)) actg <- actg[!is.na(actg$cd420), ] actg$id <- seq_len(nrow(actg)) actg$treat <- as.integer(actg$arms == 1L) actg$y_binary_neg <- as.integer(!(actg$cd420 > actg$cd40)) bin_vars <- c("hemo", "homo", "drugs", "race", "gender", "symptom") for (v in bin_vars) actg[[v]] <- as.numeric(actg[[v]]) fs_bin <- forestsearch( df.analysis = actg, outcome.name = "y_binary_neg", event.name = "y_binary_neg", treat.name = "treat", id.name = "id", confounders.name = c("age", "preanti", "wtkg", "karnof", "cd40", "cd80", bin_vars), outcome_type = "binary", effect_measure = "OR", adverse_outcome = TRUE, use_lasso = FALSE, use_grf = FALSE, use_dina = FALSE, conf.cont_jcuts = list(cd40 = 10, wtkg = 10), subgroup_method = "consistency", sg_focus = "effMaxSG", selection_rule = "neighborhood", effect_neighborhood = 0.10, effect.threshold = 1.0, consistency.threshold = 1.0, pconsistency.threshold = 0.90, consistency_method = "resample", fs.splits = 500, n.min = 60, d0.min = 10, d1.min = 10, maxk = 2, mr_inference = TRUE, mr_inference_args = list(draws = 5000L), parallel_args = list(plan = "multisession", workers = 4), seedit = 8316951 ) print(fs_bin) } ## Example 3 -- Continuous outcome, mean difference (ACTG175): the ## continuous application of León and Anderson (2026b), with the field ## (calibrated one-sided) bounds for the selected subgroup and its ## complement. Outcome is CD4 decline from baseline to week 20 (adverse ## scale); thresholds are on the mean-difference scale. if (requireNamespace("speff2trial", quietly = TRUE)) { actg <- subset(speff2trial::ACTG175, arms %in% c(1L, 3L)) actg <- actg[!is.na(actg$cd420), ] actg$id <- seq_len(nrow(actg)) actg$treat <- as.integer(actg$arms == 1L) actg$y_decline <- actg$cd40 - actg$cd420 bin_vars <- c("hemo", "homo", "drugs", "race", "gender", "symptom") for (v in bin_vars) actg[[v]] <- as.factor(actg[[v]]) fs_cont <- forestsearch( df.analysis = actg, outcome.name = "y_decline", treat.name = "treat", id.name = "id", confounders.name = c("age", "preanti", "wtkg", "karnof", "cd40", "cd80", bin_vars), outcome_type = "continuous", effect_measure = "MD", adverse_outcome = TRUE, use_lasso = FALSE, use_grf = FALSE, use_dina = FALSE, conf.cont_jcuts = list(age = 10, preanti = 10, wtkg = 10, karnof = 10, cd40 = 10, cd80 = 10), sg_focus = "effMaxSG", selection_rule = "neighborhood", effect_neighborhood = 0.20, effect.threshold = 10, consistency.threshold = 5, pconsistency.threshold = 0.90, consistency_method = "resample", fs.splits = 500, n.min = 60, d0.min = 10, d1.min = 10, maxk = 2, mr_inference = TRUE, mr_inference_args = list(ci_method = "field", draws = 5000L, include_complement = TRUE, field_complement = TRUE, field_scale_complement = "selected", return_reselection = TRUE), parallel_args = list(plan = "sequential"), seedit = 8316951 ) print(fs_cont) } ## End(Not run)
Orchestrates bootstrap analysis for ForestSearch using doFuture parallelization. Implements bias correction methods to adjust for optimism in subgroup selection.
forestsearch_bootstrap_dofuture( fs.est, nb_boots, seed = 8316951L, details = FALSE, show_three = FALSE, parallel_args = list(), digits = 4, mr_in_replicates = FALSE )forestsearch_bootstrap_dofuture( fs.est, nb_boots, seed = 8316951L, details = FALSE, show_three = FALSE, parallel_args = list(), digits = 4, mr_in_replicates = FALSE )
fs.est |
List. ForestSearch results object from |
nb_boots |
Integer. Number of bootstrap samples (recommend 500-1000). |
seed |
Integer. Random seed for reproducibility of bootstrap sample
generation. Default |
details |
Logical. If |
show_three |
Logical. If |
parallel_args |
List. Parallelization configuration with elements:
If empty list, inherits settings from original forestsearch call. |
digits |
Integer. Decimal places for construction-time numeric
formatting in |
mr_in_replicates |
Logical. Whether multiplier resampling (MR) runs
inside each bootstrap replicate. Default
The retained objects are not aggregable into an estimate for the
original analysis. Each replicate re-runs the search, selects its own
subgroup, and de-biases that subgroup – so the collection
describes MR's sampling behaviour across resampled selections, not a
better estimate of the original quantity. Averaging them, or pooling
their intervals, is an error. The bias-corrected estimate for the
original analysis is the one this function already returns in
|
An fs_bootstrap object (a list with class
c("fs_bootstrap", "list")) containing:
Data.table with bias-corrected estimates for each bootstrap iteration
List of confidence intervals for H and Hc (raw and bias-corrected)
Formatted table of subgroup estimates
Matrix (nb_boots x n) of bootstrap sample indicators
Detailed estimates for subgroup H
Detailed estimates for subgroup Hc
NULL under the default
mr_in_replicates = FALSE. Otherwise a list of length
nb_boots, element b holding replicate b's
mr_inference object (or NULL for a failed or
no-subgroup replicate), each computed against that replicate's
candidate family and selected subgroup. See the
mr_in_replicates parameter for why these must not be averaged.
(If create_summary=TRUE) Enhanced summary with tables and diagnostics
Integer. Number of bootstrap iterations requested.
Used by print.fs_bootstrap to compute identification
percentages without re-inspecting results. Added in v0.2.0.
Character vector. The subgroup identified by the
primary forestsearch call (fs.est$sg.harm),
carried forward so that print.fs_bootstrap can
report exact- and partial-match rates without retaining a
reference to fs.est. Added in v0.2.0.
Character. Outcome type of the underlying
analysis: one of "survival", "binary",
"continuous", or "count". Added in v0.2.0.
Character. Effect measure label
("HR" for survival; "OR", "RR", "RD",
"IRR", "IRD", or "MD" for GLM outcomes).
Added in v0.2.0.
Character. Estimation scale for confidence
intervals ("hr" or "1/hr"). Added in v0.2.0.
Two bias correction approaches are implemented:
Method 1 (Simple Optimism):
where is the new subgroup HR on bootstrap data and
is the new subgroup HR on original data.
Method 2 (Double Bootstrap):
where is the original subgroup HR on bootstrap data.
H: Original subgroup (harm/questionable, treat.recommend == 0)
Hc: Complement subgroup (recommend, treat.recommend == 1)
_obs: Estimate from original data
_star: Estimate from bootstrap data
_biasadj_1: Bias correction method 1
_biasadj_2: Bias correction method 2
Unlike multiplier resampling, the full bootstrap places no alignment requirement on the identifier. It replays the entire pipeline on each resample – refitting the front ends, re-enumerating the candidate family and re-running selection – rather than linearizing the selection event, so neither of MR's two conditions (selection ranking on the inferential coefficient, and a fixed candidate family) bears on its validity. In the equivalence result those conditions state, the bootstrap is the reference standard, not a party to it.
It is therefore valid for all six identifier configurations, including the
three that rank on a native statistic: DINA under
dina_select_statistic = "dina", GRF under
grf_select_statistic = "dr", and GRF under
grf_selection = "tree". For those, it correctly estimates the
selection bias that ranking on the native statistic induces on the
reported effect . That is a coherent quantity, and the
estimate of it is valid – it simply sits outside the framework the
manuscript develops, which is about identifiers whose selection map and
reported effect are the same functional.
The one place an alignment check does apply here is
mr_in_replicates = TRUE, which asks for MR inside each
replicate; see that parameter.
Typical runtime: 1-5 seconds per bootstrap iteration. For 1000 bootstraps with 6 workers, expect 3-10 minutes total. Memory usage scales with dataset size and number of workers.
Original fs.est must have identified a valid subgroup
Requires packages: data.table, foreach, doFuture,
survival
For plots: requires ggplot2
forestsearch for initial subgroup identification, and its
vocabulary section for how the full bootstrap (FB) implemented here
relates to multiplier resampling (MR)
fs_mr_inference for MR, the fast refit-free approximation to
this function
mr_estimates_table to render MR estimates in this function's
table layout
fs_fdr_report for harm-confirmation threshold sweeps
bootstrap_results for the core bootstrap worker function
build_cox_formula for Cox formula construction
fit_cox_models for Cox model fitting
## Not run: # Run ForestSearch fs_result <- forestsearch( df.analysis = mydata, outcome.name = "time", event.name = "status", treat.name = "treatment", confounders.name = c("age", "sex", "stage") ) # Run bootstrap with bias correction boot_results <- forestsearch_bootstrap_dofuture( fs.est = fs_result, nb_boots = 1000, parallel_args = list( plan = "multisession", workers = 6, show_message = TRUE ), create_summary = TRUE, create_plots = TRUE ) # View results print(boot_results$FSsg_tab) print(boot_results$summary$table) # Check success rate mean(!is.na(boot_results$results$H_biasadj_2)) ## End(Not run)## Not run: # Run ForestSearch fs_result <- forestsearch( df.analysis = mydata, outcome.name = "time", event.name = "status", treat.name = "treatment", confounders.name = c("age", "sex", "stage") ) # Run bootstrap with bias correction boot_results <- forestsearch_bootstrap_dofuture( fs.est = fs_result, nb_boots = 1000, parallel_args = list( plan = "multisession", workers = 6, show_message = TRUE ), create_summary = TRUE, create_plots = TRUE ) # View results print(boot_results$FSsg_tab) print(boot_results$summary$table) # Check success rate mean(!is.na(boot_results$results$H_biasadj_2)) ## End(Not run)
This function assesses the stability and reproducibility of ForestSearch subgroup identification through cross-validation. For each fold:
Train ForestSearch on (K-1) folds
Apply the identified subgroup to the held-out fold
Compare predictions to the original full-data analysis
forestsearch_Kfold( fs.est, Kfolds = .fs_cv_nobs(fs.est), seedit = 8316951L, parallel_args = list(plan = "multisession", workers = 6, show_message = TRUE), sg0.name = "Not recommend", sg1.name = "Recommend", details = FALSE, mr_in_replicates = FALSE )forestsearch_Kfold( fs.est, Kfolds = .fs_cv_nobs(fs.est), seedit = 8316951L, parallel_args = list(plan = "multisession", workers = 6, show_message = TRUE), sg0.name = "Not recommend", sg1.name = "Recommend", details = FALSE, mr_in_replicates = FALSE )
fs.est |
List. ForestSearch results object from |
Kfolds |
Integer. Number of folds (default: |
seedit |
Integer. Random seed for fold assignment (default: 8316951). |
parallel_args |
List. Parallelization configuration with elements:
|
sg0.name |
Character. Label for subgroup 0 (default: "Not recommend"). |
sg1.name |
Character. Label for subgroup 1 (default: "Recommend"). |
details |
Logical. Print progress details (default: FALSE). |
mr_in_replicates |
Logical. Whether multiplier resampling (MR) runs
inside each CV fold. Default
The retained objects are not aggregable into an estimate for the original analysis. Each fold re-runs the search on its training split, selects its own subgroup, and de-biases that subgroup – so the collection describes MR's behaviour across fold-specific selections, not a better estimate of the original quantity. Averaging them, or pooling their intervals, is an error. |
Performs K-fold cross-validation for ForestSearch, evaluating subgroup identification and agreement between training and test sets.
List with components:
Data frame with CV predictions for each observation
Arguments used for CV ForestSearch calls
Execution time in minutes
Percentage of folds where a subgroup was found
Original subgroup definition from full-data analysis
Subgroup labels
Number of folds used
Named vector of sensitivity metrics (sens_H, sens_Hc, ppv_H, ppv_Hc)
Named vector of subgroup-finding metrics (Any, Exact, etc.)
NULL under the default
mr_in_replicates = FALSE. Otherwise a list of length
Kfolds, element k holding fold k's
mr_inference object (or NULL), each computed against
that fold's candidate family and selected subgroup. Not
aggregable – see the mr_in_replicates parameter.
Leave-One-Out (LOO): When Kfolds = nrow(df), each
observation is held out once. Most thorough but computationally intensive.
K-Fold: When Kfolds < nrow(df), data is split into K
roughly equal folds. Good balance of bias-variance tradeoff.
The returned resCV data frame contains:
treat.recommend: Prediction from CV model
treat.recommend.original: Prediction from full-data model
cvindex: Fold assignment
sg1, sg2: Subgroup definitions found in each fold
forestsearch for initial subgroup identification
forestsearch_KfoldOut for summarizing CV results
forestsearch_tenfold for repeated K-fold simulations
## Not run: # Run initial ForestSearch fs_result <- forestsearch( df.analysis = trial_data, outcome.name = "time", event.name = "status", treat.name = "treatment", confounders.name = c("age", "biomarker") ) # Run 10-fold cross-validation cv_results <- forestsearch_Kfold( fs.est = fs_result, Kfolds = 10, parallel_args = list(plan = "multisession", workers = 4), details = TRUE ) # Summarize results cv_summary <- forestsearch_KfoldOut(cv_results, outall = TRUE) ## End(Not run)## Not run: # Run initial ForestSearch fs_result <- forestsearch( df.analysis = trial_data, outcome.name = "time", event.name = "status", treat.name = "treatment", confounders.name = c("age", "biomarker") ) # Run 10-fold cross-validation cv_results <- forestsearch_Kfold( fs.est = fs_result, Kfolds = 10, parallel_args = list(plan = "multisession", workers = 4), details = TRUE ) # Summarize results cv_summary <- forestsearch_KfoldOut(cv_results, outall = TRUE) ## End(Not run)
Summarizes cross-validation results for ForestSearch, including subgroup agreement and performance metrics.
forestsearch_KfoldOut(res, details = FALSE, outall = FALSE, digits = 4)forestsearch_KfoldOut(res, details = FALSE, outall = FALSE, digits = 4)
res |
List. Result object from ForestSearch cross-validation, must contain
elements: |
details |
Logical. Print details during execution (default: FALSE). |
outall |
Logical. If TRUE, returns all summary tables; if FALSE, returns only metrics (default: FALSE). |
digits |
Integer. Decimal places for construction-time numeric
formatting in the per-fold summary tables built when
|
If outall=FALSE, a list with sens_metrics_original and
find_metrics. If outall=TRUE, a list with summary tables and metrics.
## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") fs <- forestsearch(df, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") oob <- forestsearch_Kfold(fs.est = fs, Kfolds = nrow(df)) result <- forestsearch_KfoldOut(oob, outall = TRUE) ## End(Not run)## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") fs <- forestsearch(df, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") oob <- forestsearch_Kfold(fs.est = fs, Kfolds = nrow(df)) result <- forestsearch_KfoldOut(oob, outall = TRUE) ## End(Not run)
This function performs multiple independent K-fold cross-validations to assess the variability in subgroup identification. Each simulation:
Randomly shuffles the data
Performs K-fold CV
Records sensitivity and agreement metrics
Results are summarized across all simulations.
forestsearch_tenfold( fs.est, sims, Kfolds = 10, details = TRUE, seed = 8316951L, parallel_args = list(plan = "multisession", workers = 6, show_message = TRUE), keep_resCV = FALSE, mr_in_replicates = FALSE )forestsearch_tenfold( fs.est, sims, Kfolds = 10, details = TRUE, seed = 8316951L, parallel_args = list(plan = "multisession", workers = 6, show_message = TRUE), keep_resCV = FALSE, mr_in_replicates = FALSE )
fs.est |
List. ForestSearch results object from |
sims |
Integer. Number of simulation repetitions. |
Kfolds |
Integer. Number of folds per simulation (default: 10). |
details |
Logical. Print progress details (default: TRUE). |
seed |
Integer. Base random seed for fold shuffling. Default 8316951L. Each simulation uses seed + 1000 * ksim for reproducibility. |
parallel_args |
List. Parallelization configuration (elements
|
keep_resCV |
Logical. If |
mr_in_replicates |
Logical. Whether multiplier resampling (MR) runs
inside each CV fold. Default
The retained objects are not aggregable into an estimate for the original analysis. Each fold re-runs the search on its training split, selects its own subgroup, and de-biases that subgroup – so the collection describes MR's behaviour across fold-specific selections, not a better estimate of the original quantity. Averaging them, or pooling their intervals, is an error. |
Runs repeated K-fold cross-validation simulations for ForestSearch and summarizes subgroup identification stability across repetitions.
List with components:
Named vector of median sensitivity metrics across simulations
Named vector of median subgroup-finding metrics
Matrix of sensitivity metrics (sims x metrics)
Matrix of finding metrics (sims x metrics)
Data frame with one row per (sim, fold) combination.
Columns: sim, fold, n_test, sg1, sg2,
grf_cuts, pconsistency, training_fs_hr,
n_candidates_evaluated, any_found. The
grf_cuts column records the GRF policy-tree cut
expressions returned for each training fold (collapsed with
" | " when multiple cuts are returned; NA when GRF was
not used, failed, or returned no cuts). The pconsistency
column records the consistency probability (Pcons)
achieved by the identified subgroup on each fold (NA_real_
when no subgroup was identified or the training ForestSearch
call errored). The training_fs_hr column records the
in-sample effect estimate for the identified subgroup on the
training fold, on its NATURAL SCALE regardless of outcome type
(hazard ratio for survival; odds ratio, rate ratio, or risk
ratio for GLM ratio measures after internal exponentiation;
mean/risk difference for GLM additive measures); optimistically
biased relative to any independent estimate and surfaced for
diagnostic comparison only, with NA_real_ when no
subgroup was identified. The n_candidates_evaluated
column records the integer count of candidate subgroups
actually evaluated for consistency on this training fold
(populated whenever the consistency stage ran, even if zero
candidates met the threshold and thus no subgroup was
identified; NA_integer_ when the training call errored
or consistency evaluation did not run). Always returned
(compact; cheap). Lets you tabulate, for example, which
subgroup was identified in each fold of each simulation, the
empirical distribution of GRF cut choices across the full
sim x fold grid, the relationship between GRF's cut and the
final identified subgroup, or the near-miss consistency values
among folds that did not surface a subgroup.
List of length sims; element i is the
per-subject resCV data frame from simulation i. Only
populated when keep_resCV = TRUE; otherwise NULL.
Total execution time
Number of simulations run
Number of folds per simulation
NULL under the default
mr_in_replicates = FALSE. Otherwise a nested list: one element
per successful simulation (named sim<k>), each a list of length
Kfolds holding that fold's mr_inference object (or
NULL), computed against that fold's candidate family and
selected subgroup. Not aggregable – see the mr_in_replicates
parameter.
Unlike the single K-fold function which parallelizes across folds, this function parallelizes across simulations for better efficiency when running many repetitions. Each simulation runs its K-fold CV sequentially.
forestsearch_Kfold for single K-fold CV
forestsearch_KfoldOut for summarizing CV results
## Not run: # Run 100 repetitions of 10-fold CV tenfold_results <- forestsearch_tenfold( fs.est = fs_result, sims = 100, Kfolds = 10, parallel_args = list(plan = "multisession", workers = 6), details = TRUE ) # View summary print(tenfold_results$sens_summary) print(tenfold_results$find_summary) ## End(Not run)## Not run: # Run 100 repetitions of 10-fold CV tenfold_results <- forestsearch_tenfold( fs.est = fs_result, sims = 100, Kfolds = 10, parallel_args = list(plan = "multisession", workers = 6), details = TRUE ) # View summary print(tenfold_results$sens_summary) print(tenfold_results$find_summary) ## End(Not run)
Creates a publication-ready table from ForestSearch bootstrap results, with bias-corrected confidence intervals, informative formatting, and optional subgroup definition footnote.
format_bootstrap_table( FSsg_tab, nb_boots, est.scale = "hr", boot_success_rate = NULL, sg_definition = NULL, title = NULL, subtitle = NULL )format_bootstrap_table( FSsg_tab, nb_boots, est.scale = "hr", boot_success_rate = NULL, sg_definition = NULL, title = NULL, subtitle = NULL )
FSsg_tab |
Data frame or matrix from forestsearch_bootstrap_dofuture()$FSsg_tab |
nb_boots |
Integer. Number of bootstrap iterations performed |
est.scale |
Character. "hr" or "1/hr" for effect scale |
boot_success_rate |
Numeric. Proportion of bootstraps that found subgroups |
sg_definition |
Character. Subgroup definition string to display as footnote (e.g., "{age>=50} & {nodes>=3}"). If NULL, no subgroup footnote is added. |
title |
Character. Custom title (optional) |
subtitle |
Character. Custom subtitle (optional) |
A gt table object
## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") fs_bc <- forestsearch_bootstrap_dofuture(fs, nb_boots = 100) summaries <- summarize_bootstrap_results(fs$sg.harm, fs_bc) format_bootstrap_table(summaries$table_data) ## End(Not run)## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") fs_bc <- forestsearch_bootstrap_dofuture(fs, nb_boots = 100) summaries <- summarize_bootstrap_results(fs$sg.harm, fs_bc) format_bootstrap_table(summaries$table_data) ## End(Not run)
Formats confidence interval for estimates.
format_CI(estimates, col_names, digits = 4)format_CI(estimates, col_names, digits = 4)
estimates |
Data frame or data.table of estimates. |
col_names |
Character vector of column names for estimate, lower, upper. |
digits |
Integer. Decimal places for numeric formatting. Default 4
to provide round-down headroom for downstream display reformatting
(e.g., |
Character string formatted as \"estimate (lower, upper)\".
library(data.table) est <- data.table(hr = 1.58, lower = 0.86, upper = 2.90) format_CI(est, col_names = c("hr", "lower", "upper"))library(data.table) est <- data.table(hr = 1.58, lower = 0.86, upper = 2.90) format_CI(est, col_names = c("hr", "lower", "upper"))
Captures forestsearch(details = TRUE) console output and splits it
into two columns for readable beamer slides. Left column shows variable
selection (GRF, LASSO, candidate factors); right column shows subgroup
search, consistency evaluation, and results.
format_fs_details( fs_output, split_after = "Candidate factors", fontsize = "scriptsize", col_widths = c(0.48, 0.52), max_width = 48 )format_fs_details( fs_output, split_after = "Candidate factors", fontsize = "scriptsize", col_widths = c(0.48, 0.52), max_width = 48 )
fs_output |
Character vector of captured output lines from
|
split_after |
Character string (regex). The output is split after the
block matching this pattern. Default: |
fontsize |
Character. LaTeX font size for the output text.
One of |
col_widths |
Numeric vector of length 2. Column widths as fractions
of |
max_width |
Integer. Maximum character width per line before wrapping. Long lines are wrapped at comma or space boundaries with a 4-space continuation indent. Default: 48 (suitable for half-slide columns at scriptsize). |
Invisibly returns a list with left and right
character vectors. Side effect: emits LaTeX via cat() for use
in a chunk with results='asis'.
No special LaTeX packages required. Works in any beamer frame
without the fragile option.
In a Quarto beamer chunk with results='asis' and
echo=FALSE, first capture the forestsearch output with
capture.output(), then call format_fs_details(fs_output).
## Not run: fs_output <- capture.output( fs_res <- forestsearch( df = dat, outcome.name = "time", event.name = "status", treat.name = "trt", id.name = "id", confounders.name = confs, details = TRUE ) ) # Default: split after candidate factors format_fs_details(fs_output, fontsize = "tiny") # Custom split: after filtering summary format_fs_details(fs_output, split_after = "Found.*candidate") ## End(Not run)## Not run: fs_output <- capture.output( fs_res <- forestsearch( df = dat, outcome.name = "time", event.name = "status", treat.name = "trt", id.name = "id", confounders.name = confs, details = TRUE ) ) # Default: split after candidate factors format_fs_details(fs_output, fontsize = "tiny") # Custom split: after filtering summary format_fs_details(fs_output, split_after = "Found.*candidate") ## End(Not run)
Creates a formatted gt table from simulation operating characteristics results.
format_oc_results( results, analyses = NULL, metrics = "all", digits = 3, digits_hr = 3, title = "Operating Characteristics Summary", subtitle = NULL, use_gt = TRUE, effect_measure = NULL, dgm = NULL, subgroup_notation = c("harm", "benefit") )format_oc_results( results, analyses = NULL, metrics = "all", digits = 3, digits_hr = 3, title = "Operating Characteristics Summary", subtitle = NULL, use_gt = TRUE, effect_measure = NULL, dgm = NULL, subgroup_notation = c("harm", "benefit") )
results |
data.table or data.frame. Simulation results from
|
analyses |
Character vector. Analysis methods to include. Default: NULL (all analyses in results) |
metrics |
Character vector. Metrics to display. Options include: "detection", "classification", "hr_estimates", "ahr_estimates", "cde_estimates", "subgroup_size", "all". Default: "all" |
digits |
Integer. Decimal places for proportions. Default: 3 |
digits_hr |
Integer. Decimal places for hazard ratios. Default: 3 |
title |
Character. Table title. Default: "Operating Characteristics Summary" |
subtitle |
Character or |
use_gt |
Logical. Return gt table if TRUE, data.frame if FALSE. Default: TRUE |
effect_measure |
Character or |
dgm |
Optional DGM object ( |
subgroup_notation |
Character. |
The function summarizes simulation results across multiple metrics:
Found: Proportion of simulations finding a subgroup (any.H)
Classification: Sen, spec, PPV, NPV
HR Estimates: Mean Cox hazard ratios in true (H) and identified (H-hat) subgroups and their complements
AHR Estimates: Mean average hazard ratios (from loghr_po) in true and identified subgroups
CDE Estimates: Controlled direct effects (from theta_0/theta_1) in true and identified subgroups
Subgroup Size: Average, min, max sizes
Column notation aligns with build_estimation_table and
Leon et al. (2024): H = true (oracle) subgroup, H-hat =
identified subgroup. The asterisk (*) is reserved for bootstrap
bias-corrected estimates and is not used in this summary table.
A gt table object (if use_gt = TRUE and gt package available) or data.frame
## Not run: # format_oc_results() is called by summarize_simulation_results(). # See run_simulation_analysis() for the standard entry point. ## End(Not run)## Not run: # format_oc_results() is called by summarize_simulation_results(). # See run_simulation_analysis() for the standard entry point. ## End(Not run)
Computes an analytical approximation to the ForestSearch false positive
rate under the null hypothesis of homogeneous treatment effect. The
calculation accounts for (i) the sampling variability of the effect
estimate in a subgroup of size n_min, (ii) the number of
candidate subgroups searched (driven by the covariate space and
maxk), and (iii) the screening threshold c1.
fpr_approx( effect_true = 0.65, p_event = 0.4, n_min = 60L, c1 = 1.25, n_confounders_cont = 6L, n_confounders_bin = 6L, cont_cutoff = 4L, maxk = 2L, effect_type = c("ratio", "difference"), treat_alloc = 0.5, rho = 0.3, n_eff = NULL, thresholds = NULL, verbose = TRUE )fpr_approx( effect_true = 0.65, p_event = 0.4, n_min = 60L, c1 = 1.25, n_confounders_cont = 6L, n_confounders_bin = 6L, cont_cutoff = 4L, maxk = 2L, effect_type = c("ratio", "difference"), treat_alloc = 0.5, rho = 0.3, n_eff = NULL, thresholds = NULL, verbose = TRUE )
effect_true |
Numeric. True effect under H0 on the ratio scale (e.g., OR or HR). Default 0.65 (protective treatment). |
p_event |
Numeric in (0, 1). Overall event rate (control arm baseline rate for binary; used to compute SE). Default 0.40. |
n_min |
Integer. Minimum subgroup size ( |
c1 |
Numeric. The screening threshold on a candidate
subgroup's own effect – |
n_confounders_cont |
Integer. Number of continuous confounders. Default 6. |
n_confounders_bin |
Integer. Number of binary confounders. Default 6. |
cont_cutoff |
Integer. Number of cut points per continuous
covariate ( |
maxk |
Integer. Maximum interaction depth. 1 = single-variable subgroups only; 2 = up to two-variable interactions. Default 2. |
effect_type |
Character. |
treat_alloc |
Numeric in (0, 1). Treatment allocation fraction. Default 0.5 (1:1 randomisation). |
rho |
Numeric in [0, 1). Assumed average pairwise correlation
between candidate test statistics, used for the correlation-adjusted
FPR estimate. Default 0.30. Set to 0 for the independence
(upper-bound) estimate only. Ignored when |
n_eff |
Numeric or |
thresholds |
Numeric vector. Additional thresholds to evaluate
besides |
verbose |
Logical. Print a formatted summary. Default
|
The approximation assumes:
Normal approximation to the log-OR (or log-HR) distribution.
Independent candidates (upper bound; true FPR is lower due to overlap). A correlation-adjusted estimate is also returned.
Equal allocation to treatment arms within each subgroup.
No consistency check adjustment (the reported FPR is the
screening-stage FPR before pconsistency.threshold
filtering).
A list (class "fpr_approximation") with components:
se_log_effectSE of log(effect) in a subgroup of
size n_min.
n_candidatesTotal number of candidate subgroups searched.
n_singleNumber of single-variable candidates.
n_pairsNumber of two-variable candidates
(0 if maxk == 1).
per_candidateNamed numeric vector: P(effect > thr) for each threshold evaluated.
fpr_indepNamed numeric vector: procedure-level FPR assuming independent candidates, for each threshold.
fpr_adjustedNamed numeric vector: correlation-adjusted FPR, for each threshold.
paramsList of input parameters.
# ACTG175 binary harm search fpr_approx(effect_true = 0.65, p_event = 0.40, n_min = 60, c1 = 1.25) # Tighter threshold fpr_approx(effect_true = 0.65, p_event = 0.40, n_min = 60, c1 = 2.0) # Larger minimum subgroup fpr_approx(effect_true = 0.65, p_event = 0.40, n_min = 100, c1 = 1.25)# ACTG175 binary harm search fpr_approx(effect_true = 0.65, p_event = 0.40, n_min = 60, c1 = 1.25) # Tighter threshold fpr_approx(effect_true = 0.65, p_event = 0.40, n_min = 60, c1 = 2.0) # Larger minimum subgroup fpr_approx(effect_true = 0.65, p_event = 0.40, n_min = 100, c1 = 1.25)
Estimates the procedure-level false-positive rate (FPR) for a
forestsearch analysis at a specific
threshold pair by running nsims null simulations. No power-law
model or threshold-independence assumption is required.
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 )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 )
c1 |
Numeric scalar. The screening threshold on a candidate
subgroup's own effect. |
c2 |
Numeric scalar. The per-split effect threshold. |
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 |
fs_params |
Named list. All forestsearch() arguments except
|
nsims |
Integer. Number of null replicates. Default 200.
At FPR ~ 10%, |
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 |
null_fn |
Function or |
n_sample |
Integer or |
n_min |
Integer. Minimum subgroup size for the per-subgroup
approximation |
outcome_type_approx |
Character. One of |
d_eff_approx |
Numeric or |
prop_cens |
Numeric. Censoring proportion for survival outcomes.
Used only when |
p_event |
Numeric. Event probability for binary outcomes.
Used only when |
sigma_y |
Numeric. Residual SD for continuous outcomes. Default 1. |
sim_seed |
Integer. Base random seed. Each replicate uses
|
n_workers |
Integer. Parallel workers for the foreach loop.
Default 1 (sequential). Set to |
quiet |
Logical. Suppress progress messages. Default |
Two null-generation modes are supported:
null_fn providedEach 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.analysis only (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.name column to be present in df.analysis.
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.
The asymptotic per-subgroup at (c1, c2) is computed
via compute_detection_probability_glm() using the effective
information at n.min. The ratio
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.
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 at
(c1, c2).
d_effNumeric. Effective information used for .
L_eff_impliedNumeric. Implied from
the simulation result: .
c1, c2Numeric. The threshold pair.
NInteger. Sample size.
modeCharacter. "permutation" or "dgm".
call_argsList. Key call arguments for reproducibility.
forestsearch,
compute_detection_probability_glm,
d_eff_binary, d_eff_survival
## Not run: # ---- 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) ## End(Not run)## Not run: # ---- 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) ## End(Not run)
Uses the implied from a calibrated
fpr_calibration object to predict the procedure-level FPR at
, then optionally runs a direct
simulation at the new pair to validate the prediction.
fpr_calibration_transfer( cal_result, delta = c(0.05, 0.1, 0.2), delta_c1 = NULL, delta_c2 = NULL, nsims_check = 200L, ... )fpr_calibration_transfer( cal_result, delta = c(0.05, 0.1, 0.2), delta_c1 = NULL, delta_c2 = NULL, nsims_check = 200L, ... )
cal_result |
An object of class |
delta |
Numeric scalar. Added to both |
delta_c1 |
Numeric or |
delta_c2 |
Numeric or |
nsims_check |
Integer. Simulations to run at the new pair for empirical validation. Default 200. Set to 0 to skip. |
... |
Additional arguments passed to |
A data frame with one row per delta value containing:
c1_new, c2_new, P1_new, fpr_pred
(predicted corrected FPR using transferred L_eff_implied),
fpr_sim (direct simulation, if nsims_check > 0),
fpr_sim_lo, fpr_sim_hi (95\
(whether fpr_pred falls within the simulation CI).
## Not run: # Calibrate at base pair cal <- fpr_calibration(c1 = 1.70, c2 = 1.70, null_fn = my_dgm, n_sample = 500, fs_params = fs_binary, nsims = 200, n_min = 60, outcome_type_approx = "binary", p_event = 0.30) # Predict at base + 0.05, +0.10, +0.20 and validate transfer_results <- fpr_calibration_transfer( cal, delta = c(0.05, 0.10, 0.20), nsims_check = 200, n_workers = 4) print(transfer_results) ## End(Not run)## Not run: # Calibrate at base pair cal <- fpr_calibration(c1 = 1.70, c2 = 1.70, null_fn = my_dgm, n_sample = 500, fs_params = fs_binary, nsims = 200, n_min = 60, outcome_type_approx = "binary", p_event = 0.30) # Predict at base + 0.05, +0.10, +0.20 and validate transfer_results <- fpr_calibration_transfer( cal, delta = c(0.05, 0.10, 0.20), nsims_check = 200, n_workers = 4) print(transfer_results) ## End(Not run)
For each candidate subgroup on
fs$grp.consistency$out_sg$pareto_frontier, returns a binary
indicator (one row per subject in fs$df.est, one column per
frontier member) marking which subjects belong to that subgroup.
frontier_member_flags(fs)frontier_member_flags(fs)
fs |
A |
This lets the user compute downstream quantities the frontier table does not surface directly: per-frontier-member sample sizes by covariate stratum, set comparisons between frontier members ("which subjects are in the selected subgroup but not on the runner-up?"), and survival summaries by frontier member.
A list with three elements:
flagsInteger matrix of dimension
n_subjects x n_frontier_members, with 1 = subject
in subgroup, 0 = not.
member_labelsCharacter vector of length
n_frontier_members, each entry the
"cut1 & cut2 & ..." definition of that frontier member.
is_selectedLogical vector of length
n_frontier_members, marking which member is the selected
subgroup.
pareto_frontier_table,
compute_frontier_cis, plot_pareto_frontier.
## Not run: fmf <- frontier_member_flags(fs) # How many subjects in each frontier member? colSums(fmf$flags) # Which subjects are unique to the selected subgroup? sel <- which(fmf$is_selected) rowSums(fmf$flags) == 1L & fmf$flags[, sel] == 1L ## End(Not run)## Not run: fmf <- frontier_member_flags(fs) # How many subjects in each frontier member? colSums(fmf$flags) # Which subjects are unique to the selected subgroup? sel <- which(fmf$is_selected) rowSums(fmf$flags) == 1L & fmf$flags[, sel] == 1L ## End(Not run)
Integrity check for fs_export_maxeff_family(): the materialized membership
column of the selected candidate must equal, row for row, the per-subject
membership forestsearch itself computed
(fit$grp.consistency$sg.harm.id). A mismatch means the label-to-dummy
reconstruction is wrong, so this stops with an error rather than letting a
silently different subgroup be de-biased. fs_to_guohe() runs this check by
default (verify = TRUE).
fs_assert_membership(exp, fit)fs_assert_membership(exp, fit)
exp |
The list returned by |
fit |
The |
TRUE, invisibly, if the memberships agree; otherwise an error is
thrown.
fs_export_maxeff_family(), fs_to_guohe()
set.seed(11) n <- 200 df <- data.frame(id = seq_len(n), treat = rbinom(n, 1, 0.5), age = round(rnorm(n, 55, 12)), bm = factor(rbinom(n, 1, 0.5))) harm <- df$age <= 50 & df$bm == "1" tt <- rexp(n, 0.05 * exp(log(2.5) * df$treat * harm)) df$time <- pmin(tt, 60) df$event <- as.integer(tt <= 60) fit <- forestsearch( df.analysis = df, outcome.name = "time", event.name = "event", treat.name = "treat", id.name = "id", confounders.name = c("age", "bm"), sg_focus = "maxeff", use_grf = FALSE, use_lasso = FALSE, fs.splits = 50, n.min = 30, d0.min = 5, d1.min = 5, hr.threshold = 1.1, hr.consistency = 1.0, pconsistency.threshold = 0.5, maxk = 2, details = FALSE, plot.sg = FALSE, parallel_args = list(plan = "sequential", workers = 1L, show_message = FALSE) ) exp_fam <- fs_export_maxeff_family(fit) fs_assert_membership(exp_fam, fit)set.seed(11) n <- 200 df <- data.frame(id = seq_len(n), treat = rbinom(n, 1, 0.5), age = round(rnorm(n, 55, 12)), bm = factor(rbinom(n, 1, 0.5))) harm <- df$age <= 50 & df$bm == "1" tt <- rexp(n, 0.05 * exp(log(2.5) * df$treat * harm)) df$time <- pmin(tt, 60) df$event <- as.integer(tt <= 60) fit <- forestsearch( df.analysis = df, outcome.name = "time", event.name = "event", treat.name = "treat", id.name = "id", confounders.name = c("age", "bm"), sg_focus = "maxeff", use_grf = FALSE, use_lasso = FALSE, fs.splits = 50, n.min = 30, d0.min = 5, d1.min = 5, hr.threshold = 1.1, hr.consistency = 1.0, pconsistency.threshold = 0.5, maxk = 2, details = FALSE, plot.sg = FALSE, parallel_args = list(plan = "sequential", workers = 1L, show_message = FALSE) ) exp_fam <- fs_export_maxeff_family(fit) fs_assert_membership(exp_fam, fit)
Post-hoc, opt-in, reported diagnostics of the consistency screen, computed
from multiplier draws the package already produces. The screen admits a
candidate g when its consistency rate, rounded to pconsistency.digits,
is at least p_star; on the resample path that is exactly the standardized
statistic T(g) = (beta_hat(g) - c_cons) / sigma_D(g) clearing the
effective cutoff z_pstar = qnorm((1 + pcons_eff) / 2) (see the Rounded
admission rule section). This function reports how often the maximum of
that statistic over the candidate family would clear the screen's cutoff
under the null perturbation law (fw_size), and the cutoff that would hold
the family-wise declaration rate at alpha (kappa_hat).
fs_declaration_calibration( fit, alpha = 0.05, family = c("prereduction", "reduced"), ..., c0 = NULL ) ## S3 method for class 'fs_declaration_calibration' print(x, ...)fs_declaration_calibration( fit, alpha = 0.05, family = c("prereduction", "reduced"), ..., c0 = NULL ) ## S3 method for class 'fs_declaration_calibration' print(x, ...)
fit |
A |
alpha |
Target family-wise declaration rate, in (0, 1). Default |
family |
|
... |
Only |
c0 |
|
x |
An |
It reads a fitted object and returns a new one. It does not modify its
input, does not re-run the search, does not call the consistency engine,
and does not change what the search admitted: admitted_calibrated is the
set the calibrated rule would admit, reported beside admitted_current.
An object of class fs_declaration_calibration: a list with
kappa_hat, fw_size, alpha, p_star, digits, digits_source,
pcons_eff, z_pstar (the effective z cutoff of the rounded screen),
consistency_method, consistency_method_source, c_cons,
c_screen, B, multiplier_law, quantile_type, family_source,
family_label, n_family_prereduction, n_family_reduced,
admitted_current (the candidates the executed screen admitted; NULL
when the fit is a bare fs_mr_inference() result), admitted_calibrated,
admitted_pstar (the relabelled current rule over the same family),
screened (the candidates the consistency screen evaluated, when known),
Mstar, beta_hat, sigma_D, T_hat, column_sd, zstar_mean,
field_cor (families of at most 8 candidates, else NULL), and a
reduction list recording the replay of the near-duplicate reduction.
When c0 is given it also carries c0, a list with table (one row per
c0: c0, c0_cmp, kappa_hat, fw_size, pstar_settable,
pstar_achievable, pcons_eff_settable, z_eff_settable, z_gap,
digits_fine, pstar_fine, z_gap_fine, n_admitted_calibrated, the 0.90 / 0.95 / 0.99 quantiles of Mstar_c0,
and is_c2), admitted_calibrated (a list indexed by c0), Mstar_c0
and source ("capture" or "field_matrix"). Every other element is
unchanged by c0.
With shared multipliers xi[b, i] (one vector per draw b, reused across
every candidate) and the dfbeta influence db[g, i]:
Zstar[b, g] = sum_i xi[b, i] * db[g, i] / sigma_D(g), with
sigma_D(g)^2 = sum_i db[g, i]^2 – the robust scale the screen itself
uses, never a model-based standard error.
Mstar[b] = max_g Zstar[b, g] over the family: one-sided, in the harm
direction.
kappa_hat(alpha) is the empirical (type = 1) 1 - alpha quantile of
Mstar.
fw_size = mean(Mstar > z_pstar), z_pstar = qnorm((1 + pcons_eff) / 2).
Its threshold is the fit's own p_star at the fit's own
pconsistency.digits, not alpha: it is the family-wise size of the
screen as implemented, not of the calibrated rule.
The calibrated admission rule is
beta_hat(g) >= max(c_screen, c_cons + kappa_hat * sigma_D(g)), the
current rule with z_pstar replaced by kappa_hat.
The screen admits on round(Pcons, digits) >= p_star, with digits the
fit's pconsistency.digits. With p_star rounded up to the
10^-digits grid (g), that is Pcons >= pcons_eff = g - 0.5 * 10^-digits,
a bar below p_star itself (0.895 for p_star = 0.90, digits = 2). On
the resample path Pcons = 2 * pnorm(T) - 1, so the screen is
T >= z_pstar = qnorm((1 + pcons_eff) / 2). fw_size, admitted_pstar
and the settable p* are computed at that effective threshold; fw_size
is therefore the family-wise size of the screen as implemented and depends
on pconsistency.digits. kappa_hat does not: it is a quantile of the
maximum statistic and is compared with T directly, with no rounding.
digits is read from the fit's args_call_all$pconsistency.digits; when
absent (a bare fs_mr_inference() result, or an older fit) the
subgroup.consistency() default of 2 is used. digits_source records
which.
The correspondence with T holds only under
consistency_method = "resample". Under "split", Pcons is a split
proportion k / n_valid and admission is not a threshold on T, so
fw_size, z_pstar, admitted_pstar and the settable-p* columns are
NA / NULL rather than computed; kappa_hat and the calibrated rule are
still returned. The method is read from args_call_all$consistency_method;
a bare fs_mr_inference() result is treated as resample (the field is the
closed-form statistic), recorded in consistency_method_source.
family = "prereduction" (the default, and the only value meant for the
calibrated rule) takes the maximum over the multiplier-resampling family:
every enumerated candidate of at most maxk factors meeting the size
minimum, before any reduction keyed on fitted quantities. The
near-duplicate reduction keys on sample-fitted summaries, so a family
reduced by it is outcome-dependent; the pre-reduction family is
covariate-measurable, and is also the conservative choice (a larger family
raises the maximum). The per-arm event minima are not replayed in that
family, which makes it a superset of the one the search chose among.
family = "reduced" removes the candidates the near-duplicate reduction
removed and is a diagnostic only, so the gap is measurable. Its result is
conditional on the realized family and is labelled so; it is never meant
for admission. It needs a forestsearch fit (the reduction lives in the
identifier) and the full field matrix (keep_field_matrix = TRUE).
c1 and c2 state the claim; c0 states what the claim is protected
against: a clinically specified, pre-specified benefit level (for example
HR 0.75). The family-wise declaration rate is then controlled at alpha
whenever every candidate's true effect is at least as good as c0, rather
than only when every candidate sits at c2. With
delta_g = (c_cons - c0_cmp) / sigma_D(g):
Mstar_c0[b] = max_g { Zstar[b, g] - delta_g };
kappa_hat(c0) is its empirical (type = 1) 1 - alpha quantile;
fw_size(c0) = mean(Mstar_c0 > z_pstar), at the rounded admission rule;
admission is unchanged in form, T(g) >= kappa_hat(c0);
guidance on what to set to run kappa_hat(c0) as a p* screen at the
fit's digits: pstar_settable, the smallest p* on the 10^-digits
grid whose effective threshold is at or above kappa_hat(c0); that
threshold as pcons_eff_settable (Pcons scale) and z_eff_settable
(z scale); z_gap = z_eff_settable - kappa_hat(c0) (positive =
conservative); and digits_fine / pstar_fine / z_gap_fine, the
smallest digits (searched over 1 to 12) at which the gap falls below
0.01, with its p*. When no p* <= 1 reaches kappa_hat(c0) at the
fit's digits, pstar_achievable is FALSE and the settable columns
are NA; the print method says so. This is guidance on what to set, not
an identity: the settable screen is kappa_hat(c0) rounded up to what the
grid can express, so it admits a subset of what kappa_hat(c0) admits.
If every candidate's true effect is c0_cmp, T(g) is centred at
-delta_g, so the null law of max_g T(g) is that of the shifted maximum.
At c0 = c2, delta_g = 0 and every quantity is the unshifted one.
c0 is on the natural scale of the consistency threshold c2 (the HR on
the survival path; the ratio for OR / RR / IRR; the difference for RD /
MD) and is mapped exactly as c2 is: log() for ratio measures, identity
otherwise. c0 <= c2 is required. A non-positive c0 on a ratio path
(a log supplied by mistake) errors; on an identity path a wrong-scale
c0 cannot be detected.
The shifted maxima are read from the capture when the fit carries them
for every requested c0 (declaration_c0 at fit time); otherwise they are
computed from the stored field matrix (keep_field_matrix = TRUE);
otherwise the call errors. It never falls back to the unshifted maximum.
family = "reduced" always computes from the field matrix.
The field is retained only on request. Fit with
mr_inference = TRUE and
mr_inference_args = list(keep_declaration_field = TRUE) (add
keep_field_matrix = TRUE for the reduced diagnostic), or call
fs_mr_inference(..., keep_declaration_field = TRUE) and pass its result.
The multiplier law and B are those of the MR draws and are recorded in
the result.
fs_mr_inference(), fs_family_report(), fs_fdr_report().
## Not run: fit <- forestsearch(df, ..., mr_inference = TRUE, mr_inference_args = list(keep_declaration_field = TRUE)) fs_declaration_calibration(fit, alpha = 0.05) ## End(Not run)## Not run: fit <- forestsearch(df, ..., mr_inference = TRUE, mr_inference_args = list(keep_declaration_field = TRUE)) fs_declaration_calibration(fit, alpha = 0.05) ## End(Not run)
Draws replicates through the DGM's own generator and reports, per sample
size, how often the planted harm region would be undeclarable
(too small for the search's own size test), how often it falls under the
per-arm events floor, and how often the analysis estimand does not exist
on it.
fs_dgm_feasibility( dgm, n = c(500, 750, 1000, 2000), n.min = 60, d0.min = 10, d1.min = 10, n_rep = 200L, tolerance = 0.05, effect_measure = NULL, seed = NULL, rand_ratio = 1 ) ## S3 method for class 'fs_dgm_feasibility' print(x, ...)fs_dgm_feasibility( dgm, n = c(500, 750, 1000, 2000), n.min = 60, d0.min = 10, d1.min = 10, n_rep = 200L, tolerance = 0.05, effect_measure = NULL, seed = NULL, rand_ratio = 1 ) ## S3 method for class 'fs_dgm_feasibility' print(x, ...)
dgm |
An object of class |
n |
Numeric vector of sample sizes to evaluate. |
n.min, d0.min, d1.min
|
The search's own floors, passed so the report describes the campaign that will actually run. Defaults are the package defaults (60, 10, 10). Nothing is imposed: these are read, not applied. |
n_rep |
Integer. Replicates per sample size (default 200). |
tolerance |
Numeric. The largest undeclarable share that still counts as feasible (default 0.05). |
effect_measure |
Character or |
seed |
Integer or |
rand_ratio |
Numeric. Passed to |
x |
An |
... |
Ignored. |
The motivating case: at n = 500 a region averaging 48.5 subjects is
at or below n.min = 60 in 96\
structurally unable to recover a region of the planted size – what it
declares is a larger overlapping region. That is a property of the design,
knowable before any replicate is run, and this function is how to know it.
An object of class "fs_dgm_feasibility": a list with
$table (one row per n), $feasible (a single logical
– TRUE when every undeclarable share is at or below
tolerance), and $args.
Per n, over n_rep draws:
share_undeclarableshare with n.min.
The comparison is <=, exactly as subgroup.search() tests
it (nx <= n.min rejects), so a region must exceed
n.min to be declarable.
share_under_eventsshare with fewer than d0.min
events in the control arm or d1.min in the treated arm. Binary
and survival only – the floor is skipped entirely for continuous and
count outcomes.
share_nonestimableshare on which the estimand does not
exist, under the same per-estimand condition the estimator boundary
applies: OR needs all four cells ; RR, IRR and HR need an
event in each arm; RD, IRD and MD have no condition and this share is
always 0.
Together with per-arm cell summaries (mean, 5th and 95th percentile, and minimum of each cell across replicates).
Replicates are drawn with simulate_from_glm_dgm() – the same
function the campaign templates call – never a re-implementation, so the
population this reports on is the population a campaign would run on.
This function does not change the RNG kind. It seeds each replicate
with set.seed() under whatever generator is current and restores the
caller's RNG state on exit. The caller's own obligation is the mirror of
that one: build and calibrate the DGM before any replicate switches the
RNG kind. Calibrating a DGM after a switch to "L'Ecuyer-CMRG"
yields a different super-population, and every count here would then
describe a population no campaign ever used.
Survival DGMs are not supported, and what they would need is
this: the OC family's contract is inherits(dgm, "glm_dgm") with
a df_super carrying flag_harm. setup_gbsg_dgm()
produces a different object whose own generator is
simulate_from_dgm(dgm, n, analysis_time, cens_adjust, seed) –
a different signature, and it takes two censoring arguments that have no
GLM counterpart and that change the per-arm event counts this function
reports. Supporting it means a generator-dispatch layer plus passing
analysis_time and cens_adjust through; that is a separate
change and is deliberately not guessed at here.
simulate_from_glm_dgm, fs_oc_predict
set.seed(1) N <- 400 d <- data.frame(treat = rbinom(N, 1, 0.5), age = round(rnorm(N, 55, 12)), sex = factor(rbinom(N, 1, 0.5), levels = 0:1)) d$y <- rbinom(N, 1, plogis(-0.5 + 0.3 * d$treat)) dgm <- generate_glm_dgm(data = d, factor_vars = "sex", continuous_vars = "age", outcome_var = "y", treatment_var = "treat", outcome_type = "binary", effect_measure = "OR", subgroup_vars = "age", subgroup_cuts = list(age = list(type = "greater", quantile = 0.55)), k_inter = 1, n_super = 2000, seed = 8316951) feas <- fs_dgm_feasibility(dgm, n = c(500, 1000), n_rep = 50) feas feas$feasibleset.seed(1) N <- 400 d <- data.frame(treat = rbinom(N, 1, 0.5), age = round(rnorm(N, 55, 12)), sex = factor(rbinom(N, 1, 0.5), levels = 0:1)) d$y <- rbinom(N, 1, plogis(-0.5 + 0.3 * d$treat)) dgm <- generate_glm_dgm(data = d, factor_vars = "sex", continuous_vars = "age", outcome_var = "y", treatment_var = "treat", outcome_type = "binary", effect_measure = "OR", subgroup_vars = "age", subgroup_cuts = list(age = list(type = "greater", quantile = 0.55)), k_inter = 1, n_super = 2000, seed = 8316951) feas <- fs_dgm_feasibility(dgm, n = c(500, 1000), n_rep = 50) feas feas$feasible
Computes the finite-population sampling scale of the within-region difference-in-means estimator directly from a data generating mechanism's individual-level potential outcomes.
fs_dgm_scale( dgm, regions = NULL, harm_col = "flag_harm", rand_ratio = 1, labels = c("Q", "Qc", "S") )fs_dgm_scale( dgm, regions = NULL, harm_col = "flag_harm", rand_ratio = 1, labels = c("Q", "Qc", "S") )
dgm |
An object of class |
regions |
Optional named list of region specifications. Each element is
either a logical vector of length |
harm_col |
Character. Name of the true-region indicator column in
|
rand_ratio |
Numeric. Treatment:control randomisation ratio, so the
randomisation probability is |
labels |
Optional character vector of length 3 renaming the default
regions. Ignored when |
For a region the estimator is the within-region difference in arm
means. Conditioning on the realized arm counts,
with the region- and -free constant
where is the randomisation probability and is the total
within-arm outcome variance in under arm :
Here is the arm mean surface, the conditional outcome variance, and ,
finite-population moments over the rows of the
super-population lying in . The conditional variance is supplied by
the outcome family:
, the residual variance (constant).
.
(Poisson).
An object of class c("fs_dgm_scale", "list"):
regionsData frame, one row per region, with columns
region, n_g, P_g, m_mu0, m_mu1,
m_tau, V_mu0, V_mu1, V_tau,
C_mu0_tau, v_cond0, v_cond1, V_arm0,
V_arm1, bracket, V_eff.
sigmaResidual standard deviation for continuous outcomes,
otherwise NA_real_.
outcome_type, effect_measure
Copied from dgm.
rand_ratio, p_treat
Allocation used.
n_superSuper-population size.
At 1:1 allocation , and
bracket admits the decomposition
with the conditional average treatment effect.
The component columns are returned for every region and every outcome type;
the decomposition above is exact for continuous outcomes at 1:1 allocation.
Away from 1:1, bracket remains but no longer
equals that sum, so prefer V_eff for any downstream calculation.
V_eff treats the region size and the arm split as fixed at their
expectations. A trial draws and
, and by Jensen's inequality
and exceed their fixed-count
counterparts. Use fs_scale_se with jensen = TRUE for
the unconditional standard deviation.
Exact for identity-scale effect measures ("MD", "RD",
"IRD"), where the estimator is a difference in means. Ratio measures
("OR", "RR", "IRR") require a delta-method layer and are
rejected rather than silently approximated.
## Not run: dgm <- generate_glm_dgm(..., outcome_type = "continuous", effect_measure = "MD") sc <- fs_dgm_scale(dgm) sc$regions[, c("region", "P_g", "bracket", "V_eff")] # Standard deviation of the estimator on the true region at n = 500 fs_scale_se(sc, n = 500, region = "Q") # Arbitrary regions, e.g. a candidate family fs_dgm_scale(dgm, regions = list(big = dgm$df_super$age > 40)) ## End(Not run)## Not run: dgm <- generate_glm_dgm(..., outcome_type = "continuous", effect_measure = "MD") sc <- fs_dgm_scale(dgm) sc$regions[, c("region", "P_g", "bracket", "V_eff")] # Standard deviation of the estimator on the true region at n = 500 fs_scale_se(sc, n = 500, region = "Q") # Arbitrary regions, e.g. a candidate family fs_dgm_scale(dgm, regions = list(big = dgm$df_super$age > 40)) ## End(Not run)
Reconstructs, for every candidate in the deduplicated maxeff family kept on
a forestsearch(sg_focus = "maxeff") fit, its per-subject 0/1 membership on
the estimation frame fit$df.est. Each candidate's cut labels (e.g.
"{er <= 0}", or a negated cut "!{size <= 20}") are mapped back to the
aligned dummy columns of fit$df.est via fit$confounders.evaluated /
fit$confounders.candidate, and a candidate's membership is the
intersection (AND) over its cuts. The result is the materialized family
that guohe_algorithm3() consumes; fs_to_guohe() calls this internally.
fs_export_maxeff_family(fit, prefix = "sg_")fs_export_maxeff_family(fit, prefix = "sg_")
fit |
A |
prefix |
Character prefix for the generated membership column names
( |
A list with elements data (fit$df.est with one appended 0/1
membership column per candidate), candidates (the appended column
names), selected (the column name of the forestsearch-selected
candidate; the family table is sorted selected-first), and family (the
candidate family table fit$grp.consistency$out_sg$result).
fs_to_guohe(), fs_assert_membership()
set.seed(11) n <- 200 df <- data.frame(id = seq_len(n), treat = rbinom(n, 1, 0.5), age = round(rnorm(n, 55, 12)), bm = factor(rbinom(n, 1, 0.5))) harm <- df$age <= 50 & df$bm == "1" tt <- rexp(n, 0.05 * exp(log(2.5) * df$treat * harm)) df$time <- pmin(tt, 60) df$event <- as.integer(tt <= 60) fit <- forestsearch( df.analysis = df, outcome.name = "time", event.name = "event", treat.name = "treat", id.name = "id", confounders.name = c("age", "bm"), sg_focus = "maxeff", use_grf = FALSE, use_lasso = FALSE, fs.splits = 50, n.min = 30, d0.min = 5, d1.min = 5, hr.threshold = 1.1, hr.consistency = 1.0, pconsistency.threshold = 0.5, maxk = 2, details = FALSE, plot.sg = FALSE, parallel_args = list(plan = "sequential", workers = 1L, show_message = FALSE) ) exp_fam <- fs_export_maxeff_family(fit) head(exp_fam$candidates) exp_fam$selectedset.seed(11) n <- 200 df <- data.frame(id = seq_len(n), treat = rbinom(n, 1, 0.5), age = round(rnorm(n, 55, 12)), bm = factor(rbinom(n, 1, 0.5))) harm <- df$age <= 50 & df$bm == "1" tt <- rexp(n, 0.05 * exp(log(2.5) * df$treat * harm)) df$time <- pmin(tt, 60) df$event <- as.integer(tt <= 60) fit <- forestsearch( df.analysis = df, outcome.name = "time", event.name = "event", treat.name = "treat", id.name = "id", confounders.name = c("age", "bm"), sg_focus = "maxeff", use_grf = FALSE, use_lasso = FALSE, fs.splits = 50, n.min = 30, d0.min = 5, d1.min = 5, hr.threshold = 1.1, hr.consistency = 1.0, pconsistency.threshold = 0.5, maxk = 2, details = FALSE, plot.sg = FALSE, parallel_args = list(plan = "sequential", workers = 1L, show_message = FALSE) ) exp_fam <- fs_export_maxeff_family(fit) head(exp_fam$candidates) exp_fam$selected
A user who sets use_lasso = FALSE, use_grf = FALSE,
use_dina = FALSE may reasonably believe
the candidate family forestsearch searches is now fixed. It
is not. Continuous cuts are placed at sample quantiles
(get_FSdata()); the prevalence, redundancy and size floors
(minp, rmin, n.min) are applied to sample
counts (subgroup.search()); and the consistency stage removes
near-duplicate candidates keyed on their fitted statistics, with no
argument that turns it off (remove_near_duplicate_subgroups()).
Those stages are the method, not screening. This function says so, for a
given argument set, in a form that cannot overpromise.
fs_family_report(x, data = NULL, outcome_type = NULL) ## S3 method for class 'fs_family_report' print(x, ...)fs_family_report(x, data = NULL, outcome_type = NULL) ## S3 method for class 'fs_family_report' print(x, ...)
x |
Either a named list of |
data |
Optional data frame. When supplied, the report is grounded in
counts: the number of cut columns |
outcome_type |
One of |
... |
Ignored. |
It reports and changes nothing. It never fits a model, never runs the search, and is called by nothing else in the package. No combination of arguments makes the family deterministic while cuts are placed at sample quantiles; the best a caller can do is switch off the disableable stages, and the printed footer lists what remains.
The report mirrors forestsearch()'s own argument resolution rather
than reading arguments at face value: sg_focus aliases are
normalised; sg_focus = "maxeff" zeroes minp, rmin and
pconsistency.threshold, sets stop_threshold to NULL,
use_twostage to FALSE and max_subgroups_search to
Inf, and disables the effect floor; stop_threshold is reset
to NULL for every focus other than "maxeffCons" (it is
meaningful only there); vi.grf.min and max_n_confounders
are deprecated and always inert; and the per-arm event floors d0.min / d1.min are
skipped for continuous and count outcomes.
A data frame of class c("fs_family_report", "data.frame")
with one row per stage and columns stage, arguments (the
governing forestsearch() formals), values (their resolved
values, formatted), status (one of "deterministic",
"disabled", "inert", "data-dependent",
"data-dependent (not disableable)") and note (one line:
why, and what would change it; NA where nothing would).
Attributes: verdict (one sentence), status_counts (a
named integer vector), data_supplied (logical), and – when
data is supplied – n_cut_columns and
n_combinations.
forestsearch, fs_oc_family_enumerate
(the population-frame enumeration used by the operating-characteristic
wrapper, which is deterministic in the DGM precisely because it does not
cut at sample quantiles).
rep <- fs_family_report( list(confounders.name = c("age", "preanti", "hemo"), conf.cont_jcuts = list(age = 10, preanti = 10), n.min = 60, maxk = 2, sg_focus = "maxeffCons", use_lasso = FALSE, use_grf = FALSE, use_dina = FALSE), outcome_type = "continuous") print(rep) attr(rep, "verdict") # grounded in counts: cut columns and combinations on a data frame set.seed(1) df <- data.frame(age = round(rnorm(200, 40, 8)), preanti = round(rexp(200, 1 / 500)), hemo = rbinom(200, 1L, 0.1)) rep2 <- fs_family_report( list(confounders.name = c("age", "preanti", "hemo"), conf.cont_jcuts = list(age = 10, preanti = 10), maxk = 2, use_lasso = FALSE, use_grf = FALSE), data = df, outcome_type = "continuous") attr(rep2, "n_cut_columns"); attr(rep2, "n_combinations")rep <- fs_family_report( list(confounders.name = c("age", "preanti", "hemo"), conf.cont_jcuts = list(age = 10, preanti = 10), n.min = 60, maxk = 2, sg_focus = "maxeffCons", use_lasso = FALSE, use_grf = FALSE, use_dina = FALSE), outcome_type = "continuous") print(rep) attr(rep, "verdict") # grounded in counts: cut columns and combinations on a data frame set.seed(1) df <- data.frame(age = round(rnorm(200, 40, 8)), preanti = round(rexp(200, 1 / 500)), hemo = rbinom(200, 1L, 0.1)) rep2 <- fs_family_report( list(confounders.name = c("age", "preanti", "hemo"), conf.cont_jcuts = list(age = 10, preanti = 10), maxk = 2, use_lasso = FALSE, use_grf = FALSE), data = df, outcome_type = "continuous") attr(rep2, "n_cut_columns"); attr(rep2, "n_combinations")
Characterizes the operating false-discovery rate (FDR) of a fitted
forestsearch analysis and tests whether requiring harm
confirmation on the de-biased effect reduces it. This is a measurement
tool, not a calibration tool:
it does not solve for thresholds that control an FDR target. For the
pair the analysis actually used, it reports two quantities
under a null:
fs_fdr_report( fs_analysis, df_analysis, null = c("hom", "harm"), c_confirm = c(0.9, 1, 1.25), nsims = 1000L, n = NULL, confounders = NULL, n_super = 25000L, analysis_time = 84, cens_adjust = log(1.5), event_min = 5L, n_workers = NULL, seed_base = 8316951L, quiet = FALSE )fs_fdr_report( fs_analysis, df_analysis, null = c("hom", "harm"), c_confirm = c(0.9, 1, 1.25), nsims = 1000L, n = NULL, confounders = NULL, n_super = 25000L, analysis_time = 84, cens_adjust = log(1.5), event_min = 5L, n_workers = NULL, seed_base = 8316951L, quiet = FALSE )
fs_analysis |
A fitted |
df_analysis |
The analyst's data frame. Used only to default the
simulated sample size ( |
null |
Character vector; which null(s) to simulate. |
c_confirm |
Numeric vector of de-biased-HR harm-confirmation
thresholds at which to report B, i.e. the values |
nsims |
Integer; null replicates per null. Default 1000. |
n |
Integer or |
confounders |
Character vector or |
n_super |
Integer; super-population size for the DGM. Default 25000. |
analysis_time |
Numeric; administrative censoring time (months). Default 84. |
cens_adjust |
Numeric; censoring shift passed to
|
event_min |
Integer; minimum events for a replicate to be scored. Replicates below this (or with a single treatment arm) are counted as visible failures, never silently dropped. Default 5. |
n_workers |
Integer or |
seed_base |
Integer; base seed. Each null uses a disjoint seed block and each replicate an explicit seed, so results are plan-invariant. Default 8316951. |
quiet |
Logical; suppress progress and per-null messages. Default
|
A (raw FDR): .
B(g) (confirmed FDR): .
Because harm confirmation can only remove declarations,
always; the gap is what the de-biasing step buys at
confirmation threshold .
All thresholds and identifier knobs are inherited from
fs_analysis$args_call_all, so the FDR reported is that of the analysis
as run. Multiplier resampling is forced on internally
(mr_inference = TRUE) regardless of the fitted setting, because B
requires the per-replicate de-biased HR.
Under "hom" the treatment effect is real but uniform, so any declared
subgroup is a manufactured heterogeneity – the winner's-curse-of-search
rate. Under "harm" a real modifier exists and the flagged region
sits exactly at HR = 1.0, so a small upward fluctuation reads as harm; this
is the more adversarial null. The declared cut need not overlap
flag_harm: under either null any declaration is a false discovery.
B(g) counts a declaration only when its de-biased HR is finite and clears
g; a declaration whose de-biased HR could not be computed counts as
unconfirmed. Such cases are counted in n_deb_na and reported, so
that A - B is never mistaken for pure de-biasing benefit when part
of it is de-biased-HR unavailability.
Identifier knobs (thresholds, subgroup_method, consistency_method,
conf_force, conf.cont_jcuts, maxk, MR arguments, ...)
transfer verbatim from args_call_all. The data-binding arguments –
the five column names and the confounder set – are rebound to the DGM,
because simulate_from_dgm() emits fixed names
(y_sim / event_sim / treat_sim / id / flag_harm). mr_inference
is forced TRUE so B is computable.
An object of class "fs_fdr_report": a list with
fdrData frame, one row per null x c_confirm, with
A, A_lo, A_hi (raw FDR and 95% Wilson CI),
B, B_lo, B_hi (confirmed FDR and CI), reduction
(A - B), and the accounting columns n_valid,
n_declared, n_deb_na, fs_errors, unhandled.
hr_structureData frame of the realized hazard ratios each DGM encodes – the structural check that the nulls are different worlds.
verdict"CLEAN" if no replicate failed across any
null, else "WARNING".
n_errorsTotal failed replicates (forestsearch errors + unhandled).
c1, c2, n, nsims, nulls
The inherited thresholds and run settings.
call_argsReproducibility metadata.
forestsearch (and its vocabulary section),
fs_mr_inference for the MR step this forces on,
mr_estimates_table,
forestsearch_bootstrap_dofuture for the full bootstrap,
fpr_calibration,
setup_gbsg_dgm, calibrate_k_inter,
simulate_from_dgm
## Not run: # A fitted survival/Cox analysis (thresholds and knobs live on the object) fit <- forestsearch( df.analysis = gbsg_df, outcome.name = "time", event.name = "status", treat.name = "treat", id.name = "id", confounders.name = c("er", "age", "meno", "pgr", "nodes", "size", "grade"), is.RCT = TRUE, est.scale = "hr", subgroup_method = "consistency", consistency_method = "resample", hr.threshold = 1.25, hr.consistency = 1.0, pconsistency.threshold = 0.90, maxk = 2, mr_inference = TRUE) # Characterize its operating FDR and whether harm confirmation helps rep <- fs_fdr_report(fit, gbsg_df, nsims = 1000, n_workers = 100) rep # prints the realized-HR table and the A/B table rep$fdr # one row per null x c_confirm # Reproduce the published reference (n = 700): A ~ 0.138 (hom), 0.278 (harm) fs_fdr_report(fit, gbsg_df, n = 700, nsims = 1000, n_workers = 100) ## End(Not run)## Not run: # A fitted survival/Cox analysis (thresholds and knobs live on the object) fit <- forestsearch( df.analysis = gbsg_df, outcome.name = "time", event.name = "status", treat.name = "treat", id.name = "id", confounders.name = c("er", "age", "meno", "pgr", "nodes", "size", "grade"), is.RCT = TRUE, est.scale = "hr", subgroup_method = "consistency", consistency_method = "resample", hr.threshold = 1.25, hr.consistency = 1.0, pconsistency.threshold = 0.90, maxk = 2, mr_inference = TRUE) # Characterize its operating FDR and whether harm confirmation helps rep <- fs_fdr_report(fit, gbsg_df, nsims = 1000, n_workers = 100) rep # prints the realized-HR table and the A/B table rep$fdr # one row per null x c_confirm # Reproduce the published reference (n = 700): A ~ 0.138 (hom), 0.278 (harm) fs_fdr_report(fit, gbsg_df, n = 700, nsims = 1000, n_workers = 100) ## End(Not run)
Returns the short label used in result-file stems and plot annotations for
the rule that a given (subgroup_method, sg_focus) combination
actually runs – not the spelling the caller passed. The two differ
whenever an alias or a behavioural synonym is in play, which is precisely
when a hand-written tag goes wrong.
fs_focus_tag(subgroup_method, sg_focus)fs_focus_tag(subgroup_method, sg_focus)
subgroup_method |
Character scalar. One of |
sg_focus |
Character scalar. Any accepted |
The collapse is engine-specific:
"consistency"eff, hr and maxcons all
tag as "maxcons" – they name the consistency argmax.
maxeff and maxeffCons stay distinct, because the
consistency floor genuinely separates them here.
"dina", "grf"
eff, hr, maxcons,
maxeff and maxeffCons all tag as "eff". Neither
engine computes a Pcons, so the consistency qualifier has nothing to
bind to and all five rank by order(-eff).
The band foci collapse the same way on every engine: hrMaxSG tags as
"effMaxSG" and hrMinSG as "effMinSG". maxSG and
minSG are canonical foci in their own right and pass through.
Character scalar; the stem tag.
forestsearch for the per-engine semantics of each
focus.
fs_focus_tag("consistency", "eff") # "maxcons" fs_focus_tag("dina", "eff") # "eff" fs_focus_tag("dina", "maxeffCons") # "eff" -- not "maxeffCons" fs_focus_tag("grf", "hrMaxSG") # "effMaxSG"fs_focus_tag("consistency", "eff") # "maxcons" fs_focus_tag("dina", "eff") # "eff" fs_focus_tag("dina", "maxeffCons") # "eff" -- not "maxeffCons" fs_focus_tag("grf", "hrMaxSG") # "effMaxSG"
Applies fs_identification_structure() across a named collection of
bundles – typically one per trial size – and returns a single object the
plot and table helpers consume. This is the package-level replacement for
the inline fs_identification_figures*.qmd fragments.
fs_identification_summary( bundles, anchor, partner, proxy, confounders = NULL, sweep_label = "Trial size (n)", sg_def_col = "sg_def", covs_col = "covs" )fs_identification_summary( bundles, anchor, partner, proxy, confounders = NULL, sweep_label = "Trial size (n)", sg_def_col = "sg_def", covs_col = "covs" )
bundles |
A named list. Each element is a results data frame, a bundle
carrying |
anchor, partner, proxy
|
Character. The planted-structure triple. |
confounders |
Character vector. The analysis covariate pool. |
sweep_label |
Character. Axis label for the sweep dimension. |
sg_def_col, covs_col
|
Character. Passed through. |
An object of class fs_identification: a list with per_point
(the raw aggregates), tidy involvement and structure data frames, an
accuracy data frame, and the triple it was built with.
## Not run: id <- fs_identification_summary( list(`500` = "results/fs_..._n500_combined_1_500.rds", `1000` = "results/fs_..._n1000_combined_1_500.rds"), anchor = "er", partner = "meno", proxy = "age", confounders = c("er", "age", "meno", "pgr", "nodes", "size", "grade")) plot_fs_identification_involvement(id) plot_fs_identification_structure(id) fs_identification_table(id) ## End(Not run)## Not run: id <- fs_identification_summary( list(`500` = "results/fs_..._n500_combined_1_500.rds", `1000` = "results/fs_..._n1000_combined_1_500.rds"), anchor = "er", partner = "meno", proxy = "age", confounders = c("er", "age", "meno", "pgr", "nodes", "size", "grade")) plot_fs_identification_involvement(id) plot_fs_identification_structure(id) fs_identification_table(id) ## End(Not run)
The supplement's structure table: detection rate, the five structure shares,
anchor recovery, and the conditional partner-versus-proxy split, one column
per sweep point. Returned as a data frame by default so a LaTeX document can
style it however it likes; as_gt = TRUE renders it with gt.
fs_identification_table(x, as_gt = FALSE, digits = 1)fs_identification_table(x, as_gt = FALSE, digits = 1)
x |
An |
as_gt |
Logical. Render with gt instead of returning the data frame. |
digits |
Integer. Digits on the percent labels. |
A data frame, or a gt_tbl. The accuracy sentence the supplement
carries as a table footnote is attached as the "accuracy_note"
attribute of the data frame.
Converts q-indexed codes to human-readable labels using the confs_labels mapping. Supports both full format ("q1.1", "q3.0") and short format ("q1", "q3"). Handles vector input via recursion.
FS_labels(Qsg, confs_labels)FS_labels(Qsg, confs_labels)
Qsg |
Character. Factor code in format |
confs_labels |
Character vector. Labels for each factor, indexed by factor number. |
Character. Human-readable label wrapped in braces, e.g.,
"{age <= 50}" or "!{age <= 50}" for complement. Returns the
original code if no match is found.
## Not run: labels <- c("age <= 50", "tumor size <= 20", "nodes <= 3") FS_labels("q1.1", labels) FS_labels("q1.0", labels) FS_labels("q2", labels) FS_labels(c("q1", "q3"), labels) ## End(Not run)## Not run: labels <- c("age <= 50", "tumor size <= 20", "nodes <= 3") FS_labels("q1.1", labels) FS_labels("q1.0", labels) FS_labels("q2", labels) FS_labels(c("q1", "q3"), labels) ## End(Not run)
Summarises a simulation payload produced by the multiplier-resampling (MR) harness into the estimation, coverage and classification quantities used in simulation summaries, returning plain numbers rather than a formatted table.
fs_mr_oc_summary( payload, estimators = NULL, blocks = c("H", "Hc"), digits = NULL )fs_mr_oc_summary( payload, estimators = NULL, blocks = c("H", "Hc"), digits = NULL )
payload |
A list with |
estimators |
Character vector selecting estimators, from
|
blocks |
Character vector of blocks, from |
digits |
Integer or |
An object of class c("fs_mr_oc", "list"):
estimationData frame, one row per block and estimator:
n, avg, sd_emp, se_hat, the four
bias_*, the four cov_*, and width.
identificationData frame with one row per convention: detection rate, mean subgroup size, and mean sens/spec/ppv/npv.
targetsThe oriented target values used.
metaKey run metadata, echoed.
Estimator columns (or_, nv_, fb_, mr_) are stored
on the ORIENTED scale, where positive means harm. betaHhat_H and
betaHhat_Hc are stored on the RAW outcome scale. The bridge is
orient = if (adverse_outcome) 1 else -1, applied here as the
simulation harness applies it. Getting this backwards silently negates every
bias.
Every estimator is scored against every target in both blocks, so that disagreement is visible rather than assumed away:
betaPer-replicate exact – the
conditional estimand at the realised region.
oraclePer-replicate refit on the true region in that trial.
structScalar structural potential-outcome effect in the true region; zero Monte Carlo error.
margScalar fitted effect in the true region on one realised draw; carries sampling noise.
Bias is ABSOLUTE, in outcome units. On an identity scale a complement effect can sit near zero, so a relative bias would explode.
Both are returned, because the project contains both and they disagree whenever detection is not near certain:
conditionalAveraged over DETECTED replicates only. This is the detected-only summary.
unconditionalAveraged over ALL replicates, following
build_classification_table() and Leon et al. (2024, Table 1),
with non-detection scored as sens = 0, ppv = 0, spec = 1, npv = 1.
With detection at 999/1000 the two agree to three decimals; in a null cell at 0.930 they do not. Quote which one you mean.
## Not run: oc <- fs_mr_oc_summary("fs_maxeffCons_mr_md40_knoise0_n500_res_1_1000.rds") oc$estimation[oc$estimation$block == "H", c("estimator", "avg", "bias_beta")] oc$identification ## End(Not run)## Not run: oc <- fs_mr_oc_summary("fs_maxeffCons_mr_md40_knoise0_n500_res_1_1000.rds") oc$estimation[oc$estimation$block == "H", c("estimator", "avg", "bias_beta")] oc$identification ## End(Not run)
Builds the family of candidate subgroups that forestsearch
would search over, but on the DGM's super-population frame
dgm$df_super rather than on a sampled trial, so that every cut lands
at a population quantile and every prevalence, purity and overlap is a
population proportion. The result carries the nine quantities the
operating-characteristics prediction of fs_oc_predict consumes.
fs_oc_family_enumerate( dgm, forestsearch_args, n, max_M = 2000L, verbose = FALSE )fs_oc_family_enumerate( dgm, forestsearch_args, n, max_M = 2000L, verbose = FALSE )
dgm |
An object of class |
forestsearch_args |
Named list of |
n |
Integer. Trial size at which the family is evaluated: sets the
size floor |
max_M |
Integer. Size guard: if more than |
verbose |
Logical. Print the enumeration counts at each stage. |
Cuts. The cut-related entries of forestsearch_args
(confounders.name, conf.cont_jcuts, cut_type,
cont.cutoff, conf.cont_medians, conf.cont_medians_force,
conf_force, defaultcut_names, exclude_cuts,
collapse_cuts, collapse_cuts_args) are handed to the package's
own get_FSdata() on df_super. Both directions of every cut are
generated, as the search does. LASSO, GRF and DINA cut sources are off: they
are sample-fitted screens, not part of the cut specification.
Combinations. All combinations of up to maxk indicator
columns are enumerated with generate_combination_indices(), in the
order forestsearch() uses when it composes the MR family.
Structural floors (all on the population frame, in the order of
evaluate_combination_with_status()):
combinations with a constant column or an empty pairwise intersection are skipped (the search's status 0);
minp: every constituent factor must have population
prevalence >= minp;
rmin: each added factor must shrink the membership by more
than rmin subjects of a trial of size n, i.e. by more than
rmin / n in population proportion;
size: Pg >= n.min / n, where n.min is resolved as
forestsearch() resolves it – the supplied value (default 60), or
max(60, ceiling(n.min.frac * n)) when n.min = NULL.
The GRF variable-importance pre-screen, the effect screen, the consistency screen and the near-duplicate removal are not applied. Candidates whose population membership vectors are identical are collapsed to the first one enumerated.
Null DGMs. A DGM whose df_super$flag_harm has no member
(Q empty; generate_glm_dgm(model = "null")) is detected
structurally, cross-checked against dgm$model when present. Under
the null every candidate has the same true effect, so the fields become:
beta_g = the common effect (the DGM's effect_Qc =
effect_ITT, oriented as the alternative path orients);
se_g from the whole-population effective variance
(fs_dgm_scale(dgm, regions = list(S = ...)), the S row) at
(n, Pg) with the same prevalence scaling; PQg = 0;
sens_g = NA (0/0, undefined – not zero); spec_g = 1 - Pg;
PQ = 0 (from which NPV = 1 follows downstream). Enumeration, floors,
ovl and the covariance are unchanged. The element null
records the branch taken.
Scale. Every mean and standard error is derived from
fs_dgm_scale(dgm). The orientation is the harm direction
s = sign(m_tau[Q]), so the planted effect is oriented positive:
with Q the true harm region, tauQc = s * m_tau[Qc],
bint = s * (m_tau[Q] - m_tau[Qc]),
seQ1000 = sqrt(V_eff[Q] / (1000 * P(Q))), and for each candidate
beta_g = tauQc + bint * PQg – the signed mixture
s * (m_tau[Qc] + (m_tau[Q] - m_tau[Qc]) * PQg) – and
se_g = seQ1000 * sqrt(1000 / n) * sqrt(P(Q) / Pg) (sign-free, from
V_eff[Q]). Opposite-sign families (sign(m_tau[Qc]) != s)
are supported: benefit-direction candidates carry oriented-negative
beta_g, and tauQc may be negative for such families. When
both region effects share a sign the values coincide exactly with the
former oriented-absolute reading. A DGM with m_tau[Q] exactly zero
is rejected (no harm direction to orient by): plant a nonzero Q effect or
use the null path.
An object of class c("fs_oc_family", "list") with elements
labCharacter, length M: the rule of each candidate.
PgPopulation prevalence P(g).
PQgPurity P(g & Q) / P(g).
beta_gOriented mixture mean
s * (m_tau[Qc] + (m_tau[Q] - m_tau[Qc]) * PQg), with
s = sign(m_tau[Q]) the harm direction.
se_gAnchored standard error at n.
sens_gP(g & Q) / P(Q).
spec_g1 - P(g & Qc) / P(Qc).
ovlM-by-M matrix of P(g_i & g_j).
MNumber of candidates.
PQP(Q), the prevalence of the true region.
membLogical matrix, nrow(df_super) by M, the
population membership of each candidate.
nullLogical: TRUE when the null branch was
taken (Q empty).
orientationAlternative branch only: list with the
harm-direction sign s, the signed region effects
m_tau_Q and m_tau_Qc, and the oriented mixture
coefficients tauQc = s * m_tau_Qc (may be negative for
opposite-sign families) and bint = s * (m_tau_Q - m_tau_Qc).
Absent on the null branch.
scaleThe fs_dgm_scale object used.
nThe trial size.
args_usedThe forestsearch_args entries consumed,
with the resolved n.min.
cutsThe cut expressions get_FSdata() produced.
countsNamed integer vector: candidates at each stage.
set.seed(1) N <- 2000 age <- round(rnorm(N, 35, 9)); pre <- round(rexp(N, 1 / 500)) V <- factor(rbinom(N, 1, 0.42), levels = 0:1) inQ <- as.integer(age > 34 & pre <= 745) mu0 <- 40 + 0.2 * age dgm <- structure(list( df_super = data.frame(age = age, preanti = pre, V = V, mu0 = mu0, mu1 = mu0 - 26 - 14 * inQ, flag_harm = inQ), outcome_type = "continuous", effect_measure = "MD", model_params = list(sigma = 127.5)), class = c("glm_dgm", "list")) fam <- fs_oc_family_enumerate( dgm, list(confounders.name = c("age", "preanti", "V"), conf.cont_jcuts = list(age = 4, preanti = 4), n.min = 60), n = 500) famset.seed(1) N <- 2000 age <- round(rnorm(N, 35, 9)); pre <- round(rexp(N, 1 / 500)) V <- factor(rbinom(N, 1, 0.42), levels = 0:1) inQ <- as.integer(age > 34 & pre <= 745) mu0 <- 40 + 0.2 * age dgm <- structure(list( df_super = data.frame(age = age, preanti = pre, V = V, mu0 = mu0, mu1 = mu0 - 26 - 14 * inQ, flag_harm = inQ), outcome_type = "continuous", effect_measure = "MD", model_params = list(sigma = 127.5)), class = c("glm_dgm", "list")) fam <- fs_oc_family_enumerate( dgm, list(confounders.name = c("age", "preanti", "V"), conf.cont_jcuts = list(age = 4, preanti = 4), n.min = 60), n = 500) fam
Evaluates every fs_oc_predict() quantity over the full crossing of
n, c1 and c2, enumerating the family and drawing the
candidates' fitted effects once per n (and per gate) and
sweeping the thresholds against those draws. Thresholds enter only the
gate, so the sweep costs arithmetic, not draws.
fs_oc_grid( dgm = NULL, forestsearch_args = list(), n, c1, c2, family = NULL, consistency_method = c("resample", "split"), pconsistency = NULL, draws = 2e+05, block = 50000, seed = NULL, verbose = FALSE, ... )fs_oc_grid( dgm = NULL, forestsearch_args = list(), n, c1, c2, family = NULL, consistency_method = c("resample", "split"), pconsistency = NULL, draws = 2e+05, block = 50000, seed = NULL, verbose = FALSE, ... )
dgm |
An object of class |
forestsearch_args |
Named list of |
n |
Numeric vector of trial sizes. |
c1, c2
|
Numeric vectors of screening and consistency floors; the grid
is their full crossing with |
family |
|
consistency_method |
|
pconsistency |
Numeric in (0, 1). Consistency-rate threshold
for the |
draws |
Integer. Number of Monte-Carlo draws. |
block |
Integer or |
seed |
Integer or |
verbose |
Logical. Report per- |
... |
Passed to |
For each n the family is enumerated with
fs_oc_family_enumerate (or the supplied family is used
when its n matches), the draw set is generated under seed
(the same seed for every n: common random numbers across trial
sizes), and every (c1, c2) is evaluated on it. With
block = Inf the whole draw set is held and the computation is exactly
fs_oc_predict's – one grid point is identical() to a
call of fs_oc_predict() at the same settings and seed. With a finite
block the draws are generated in row blocks of that size and the
quantities accumulated, so memory is O(block x M) instead of O(draws x M);
blocked results agree with the one-block results to Monte-Carlo precision
(the RNG stream is laid out differently and proportions are formed as
count / draws), not bit-for-bit.
An object of class c("fs_oc_grid", "list"):
tableData frame, one row per (n, gate, c1, c2):
n, consistency_method, pconsistency, c1, c2, M, draws, block,
seed, det_rate, det_rate_se, EnH, Esens, Espec, Eppv, Enpv, EbetaH,
Enaive_bias, mass_below.
resultsList, parallel to the rows of table, of the
full per-point objects (class fs_oc_predict, with P1,
p_sel, sel_c and their MC SEs).
familiesPer n: M, the floor, the stage
counts and the family.
timingPer n and gate: seconds for enumeration,
drawing and sweeping.
settingsThe call's settings.
piQ <- 0.34 fam <- structure(list( lab = c("Q", "P", "D"), Pg = c(piQ, 0.45, 0.31), PQg = c(1, 0.28 / 0.45, 1), sens_g = c(1, 0.28 / piQ, 0.31 / piQ), spec_g = c(1, 1 - 0.17 / (1 - piQ), 1), ovl = matrix(c(piQ, 0.28, 0.31, 0.28, 0.45, 0.28 * 0.31 / piQ, 0.31, 0.28 * 0.31 / piQ, 0.31), 3, 3), M = 3L, PQ = piQ, n = 500), class = c("fs_oc_family", "list")) fam$beta_g <- 26 + 14 * fam$PQg fam$se_g <- 13.7 * sqrt(2) * sqrt(piQ / fam$Pg) g <- fs_oc_grid(family = fam, n = 500, c1 = c(20, 30, 40), c2 = 10, consistency_method = "resample", draws = 2e4, seed = 1) gpiQ <- 0.34 fam <- structure(list( lab = c("Q", "P", "D"), Pg = c(piQ, 0.45, 0.31), PQg = c(1, 0.28 / 0.45, 1), sens_g = c(1, 0.28 / piQ, 0.31 / piQ), spec_g = c(1, 1 - 0.17 / (1 - piQ), 1), ovl = matrix(c(piQ, 0.28, 0.31, 0.28, 0.45, 0.28 * 0.31 / piQ, 0.31, 0.28 * 0.31 / piQ, 0.31), 3, 3), M = 3L, PQ = piQ, n = 500), class = c("fs_oc_family", "list")) fam$beta_g <- 26 + 14 * fam$PQg fam$se_g <- 13.7 * sqrt(2) * sqrt(piQ / fam$Pg) g <- fs_oc_grid(family = fam, n = 500, c1 = c(20, 30, 40), c2 = 10, consistency_method = "resample", draws = 2e4, seed = 1) g
Finds the threshold – c1 at fixed c2, or c2 at fixed
c1 – at which the family declaration rate equals target, on
one fixed draw set: "the c1 giving 80\
read as a type-I error under a null DGM.
fs_oc_invert( dgm = NULL, forestsearch_args = list(), n, target, solve_for = c("c1", "c2"), c1 = NULL, c2 = NULL, family = NULL, consistency_method = c("resample", "split"), pconsistency = NULL, draws = 2e+05, seed = NULL, tol = 0.001, ... )fs_oc_invert( dgm = NULL, forestsearch_args = list(), n, target, solve_for = c("c1", "c2"), c1 = NULL, c2 = NULL, family = NULL, consistency_method = c("resample", "split"), pconsistency = NULL, draws = 2e+05, seed = NULL, tol = 0.001, ... )
dgm |
An object of class |
forestsearch_args |
Named list of |
n |
Single trial size. |
target |
Numeric in (0, 1); may be a vector when
|
solve_for |
|
c1, c2
|
The fixed threshold (the one not solved for; the solved-for
argument is ignored). Defaults from |
family |
|
consistency_method |
One gate. |
pconsistency |
Numeric in (0, 1). Consistency-rate threshold
for the |
draws |
Integer. Number of Monte-Carlo draws. |
seed |
Integer or |
tol |
Bisection tolerance on the threshold scale ( |
... |
Passed to |
Solving for c1: an order statistic. At fixed
(n, gate, c2) neither the eligible set nor the winner depends on
c1: with eligible_g = (Bhat_g - c2 >= z_p * se_g)
("resample") or (W1_g >= c2) & (W2_g >= c2) ("split"),
each draw's winner is w = argmax_{eligible} Bhat_g and its statistic
T = Bhat_w (-Inf when nothing is eligible). Declaration at
c1 is exactly T >= c1, and w is the maxeffCons winner
whenever anything declares. So the reduction is computed once
(.fs_oc_reduce()), the declaration rate at any c1 is
mean(T >= c1), and the largest c1 attaining a target is the
k-th largest T with k = ceiling(target * draws): an
order statistic, no search. The ceiling is mean(T > -Inf),
the fraction of draws with any eligible member, set by the consistency
screen alone; a target above it is unattainable and returns
NA with the ceiling and the binding threshold named – no
extrapolation. target may be a vector: one reduction serves every
target.
Solving for c2 at fixed c1: the eligible set moves
with c2, so the rate is bracketed from the draws' range and bisected
to tol on fixed draws (a monotone non-increasing step function); the
returned value is the largest threshold whose rate is still at least
target.
Every result reports the achieved rate and its MC SE, so the resolution the draw count supports is visible.
For a single target, an object of class
c("fs_oc_invert", "list"): value (the threshold, or
NA), achieved (rate at value), achieved_se,
target, ceiling, ceiling_se, binding (which
threshold sets the ceiling), attainable, solve_for,
fixed, k (the order-statistic rank, solve_for = "c1"),
next_step_rate, iterations (bisections, solve_for =
"c2"), settings. For a vector target, a list of such
objects (class fs_oc_invert_list) with a table attribute.
piQ <- 0.34 fam <- structure(list( lab = c("Q", "P", "D"), Pg = c(piQ, 0.45, 0.31), PQg = c(1, 0.28 / 0.45, 1), sens_g = c(1, 0.28 / piQ, 0.31 / piQ), spec_g = c(1, 1 - 0.17 / (1 - piQ), 1), ovl = matrix(c(piQ, 0.28, 0.31, 0.28, 0.45, 0.28 * 0.31 / piQ, 0.31, 0.28 * 0.31 / piQ, 0.31), 3, 3), M = 3L, PQ = piQ, n = 500), class = c("fs_oc_family", "list")) fam$beta_g <- 26 + 14 * fam$PQg fam$se_g <- 13.7 * sqrt(2) * sqrt(piQ / fam$Pg) fs_oc_invert(family = fam, n = 500, target = 0.5, solve_for = "c1", c2 = 10, consistency_method = "resample", draws = 2e4, seed = 1)piQ <- 0.34 fam <- structure(list( lab = c("Q", "P", "D"), Pg = c(piQ, 0.45, 0.31), PQg = c(1, 0.28 / 0.45, 1), sens_g = c(1, 0.28 / piQ, 0.31 / piQ), spec_g = c(1, 1 - 0.17 / (1 - piQ), 1), ovl = matrix(c(piQ, 0.28, 0.31, 0.28, 0.45, 0.28 * 0.31 / piQ, 0.31, 0.28 * 0.31 / piQ, 0.31), 3, 3), M = 3L, PQ = piQ, n = 500), class = c("fs_oc_family", "list")) fam$beta_g <- 26 + 14 * fam$PQg fam$se_g <- 13.7 * sqrt(2) * sqrt(piQ / fam$Pg) fs_oc_invert(family = fam, n = 500, target = 0.5, solve_for = "c1", c2 = 10, consistency_method = "resample", draws = 2e4, seed = 1)
Monte-Carlo prediction of the maxeffCons search's operating characteristics
– family declaration rate, per-candidate declaration, the selection
distribution, expected selected-subgroup size, sensitivity, specificity,
PPV, NPV, the oriented effect on the selected rule and the naive selection
bias – from the joint normal law of the candidates' fitted effects. The
family is a population enumeration from fs_oc_family_enumerate
or a supplied fs_oc_family object.
fs_oc_predict( dgm = NULL, forestsearch_args = list(), n, c1 = NULL, c2 = NULL, family = NULL, consistency_method = c("resample", "split"), pconsistency = NULL, draws = 2e+05, seed = NULL, ... )fs_oc_predict( dgm = NULL, forestsearch_args = list(), n, c1 = NULL, c2 = NULL, family = NULL, consistency_method = c("resample", "split"), pconsistency = NULL, draws = 2e+05, seed = NULL, ... )
dgm |
An object of class |
forestsearch_args |
Named list of |
n |
Integer. Trial size. Overrides any size implied by the arguments; sets the size floor and the standard-error scale of an enumerated family and converts expected prevalence to expected subjects. |
c1 |
Numeric. Screening floor on the full-sample effect – the |
c2 |
Numeric. Consistency floor on each half-sample effect – the
|
family |
|
consistency_method |
|
pconsistency |
Numeric in (0, 1). Consistency-rate threshold
for the |
draws |
Integer. Number of Monte-Carlo draws. |
seed |
Integer or |
... |
Passed to |
The full-sample fitted effects of the M candidates are taken as
with ,
. Two independent
half-sample draws W1, W2, each with covariance 2 * Sg,
are generated through fs_sym_root; the full-sample effect is
Bhat = (W1 + W2) / 2.
The two gates. Family declaration is any candidate declaring; the selected rule is the effect maximiser among declaring candidates (maxeffCons).
"resample" – the package's production screenThe
consistency stage of forestsearch (default
consistency_method = "resample") represents the random 50/50
split's half effects as and computes the
consistency rate in closed form as
, a candidate passing when
that rate is at least pconsistency
(R/consistency_resample.R; R/subgroup_consistency_helpers.R
drops a candidate when p.consistency < pconsistency.threshold).
Inverting: with
. is
sqrt(sum(dfbeta[i, treat]^2)), the sandwich standard error of the
subgroup treatment coefficient; in the draw model above the analogous
quantity is se_g, and on a simulated MD40 trial the two agree
within a few percent with no prevalence trend, so the
wrapper identifies . The gate is then a single
threshold on the full-sample draw,
(Bhat >= c1) & (Bhat - c2 >= z_p * se_g), and only
Bhat ~ N(beta_g, Sg) is drawn – one matrix, not two.
"split" – the analytic document's historical gateOne
half-sample pair W1, W2 stands in for the consistency
rate: (W1 + W2 >= 2 * c1) & (W1 >= c2) & (W2 >= c2), the
screening floor c1 on the full-sample effect and the consistency
floor c2 on each half. Kept bit-identical to the document's
worked-predictions chunk.
All expectations are selection-weighted population functionals of the
family, conditional on declaration. Monte-Carlo standard errors are given
for the proportions (sqrt(p (1 - p) / draws)).
An object of class c("fs_oc_predict", "list"):
det_rate, det_rate_se
Family declaration rate and its MC standard error.
P1, P1_se
Per-candidate declaration probability.
p_sel, p_sel_se
Per-candidate selection probability (unconditional).
sel_cSelection distribution given declaration.
EnHExpected selected-subgroup size in subjects,
n * sum(sel_c * Pg).
Esens, Espec, Eppv, Enpv
Expected classification metrics of the selected rule against Q.
EbetaHExpected oriented true effect on the selected rule.
Enaive_biasExpected naive minus true effect on the selected rule.
mass_belowSelection mass on rules whose true mean is
below c1.
M, lab
Family size and labels.
settingsn, c1, c2,
consistency_method, pconsistency (NA for
"split"), draws.
seedThe seed used.
familyThe family object.
fs_oc_family_enumerate, fs_sym_root
# A hand-built three-candidate family piQ <- 0.34 fam <- structure(list( lab = c("Q", "P", "D"), Pg = c(piQ, 0.45, 0.31), PQg = c(1, 0.28 / 0.45, 1), sens_g = c(1, 0.28 / piQ, 0.31 / piQ), spec_g = c(1, 1 - 0.17 / (1 - piQ), 1), ovl = matrix(c(piQ, 0.28, 0.31, 0.28, 0.45, 0.28 * 0.31 / piQ, 0.31, 0.28 * 0.31 / piQ, 0.31), 3, 3), M = 3L, PQ = piQ), class = c("fs_oc_family", "list")) fam$beta_g <- 26 + 14 * fam$PQg fam$se_g <- 13.7 * sqrt(2) * sqrt(piQ / fam$Pg) fs_oc_predict(family = fam, n = 500, c1 = 30, c2 = 10, consistency_method = "resample", draws = 2e4, seed = 1) fs_oc_predict(family = fam, n = 500, c1 = 30, c2 = 10, consistency_method = "split", draws = 2e4, seed = 1)# A hand-built three-candidate family piQ <- 0.34 fam <- structure(list( lab = c("Q", "P", "D"), Pg = c(piQ, 0.45, 0.31), PQg = c(1, 0.28 / 0.45, 1), sens_g = c(1, 0.28 / piQ, 0.31 / piQ), spec_g = c(1, 1 - 0.17 / (1 - piQ), 1), ovl = matrix(c(piQ, 0.28, 0.31, 0.28, 0.45, 0.28 * 0.31 / piQ, 0.31, 0.28 * 0.31 / piQ, 0.31), 3, 3), M = 3L, PQ = piQ), class = c("fs_oc_family", "list")) fam$beta_g <- 26 + 14 * fam$PQg fam$se_g <- 13.7 * sqrt(2) * sqrt(piQ / fam$Pg) fs_oc_predict(family = fam, n = 500, c1 = 30, c2 = 10, consistency_method = "resample", draws = 2e4, seed = 1) fs_oc_predict(family = fam, n = 500, c1 = 30, c2 = 10, consistency_method = "split", draws = 2e4, seed = 1)
Draws the standard bias-vs-coverage display from a stacked
fs_sim_bias_coverage() table carrying a cell column: (1) one-sided
coverage against b with Gaussian-reference curves Phi(z1 * r - b) for
the given curves values of r; (2) two-sided coverage against b with
curves Phi(z2 * r - b) - Phi(-z2 * r - b); (3) observed coverage against
the Gaussian reference at each point's own (b, r), with the diagonal.
Estimators are distinguished by marker shape, the nominal level by a
dashed line, and cells by text labels.
fs_plot_bias_coverage( tbl, labels = TRUE, curves = c(1, 1.25, 1.5, 2), level = 0.95, side = c("lower", "upper") )fs_plot_bias_coverage( tbl, labels = TRUE, curves = c(1, 1.25, 1.5, 2), level = 0.95, side = c("lower", "upper") )
tbl |
Stacked output of |
labels |
Draw cell labels next to the points (default |
curves |
Gaussian-reference |
level |
Nominal level for the reference lines (default 0.95). |
side |
|
A patchwork object of the three ggplot panels.
Collapses realized rules to the set of covariates they name, discarding
the cut values. A rules-by-frequency table lists every distinct sg_def
string and so is dominated by cut noise – a 500-replicate survival run
produced 583 rows, in which {age <= 46} & {nodes <= 5} and
{nodes <= 3} & {age <= 46} are separate entries despite being the same
pairing. This reports how often each covariate combination is selected,
which is the structural question.
fs_rule_covariate_pairs(x, true_covariates = NULL, detected_only = TRUE)fs_rule_covariate_pairs(x, true_covariates = NULL, detected_only = TRUE)
x |
A results data frame carrying |
true_covariates |
Character vector. The true-region covariates, used to
classify each signature as |
detected_only |
Logical. Restrict to rows with |
A data frame ordered by frequency: covariates, k, n, share,
n_rules, and match when true_covariates is given.
The covariate names in the rule, de-duplicated and sorted, joined by " & ".
Sorting is what makes the two orderings above collapse together. Two
consequences worth knowing:
A rule naming one covariate twice — a band such as
!{nodes <= 3} & {nodes <= 5} — has signature "nodes", of size 1. The
k column gives the number of distinct covariates, and n_rules the
number of distinct rule strings that collapsed into the row, so bands are
visible rather than hidden.
Logically equivalent spellings of the same condition (!{meno} versus
{meno == 0}) collapse together, which a raw rule table also splits.
Converts a fs_dgm_scale object into the sampling standard
deviation of the within-region difference-in-means estimator at trial size
n.
fs_scale_se(scale, n, region = NULL, jensen = FALSE)fs_scale_se(scale, n, region = NULL, jensen = FALSE)
scale |
An object of class |
n |
Integer. Trial size. |
region |
Character. Region name, matching a row of
|
jensen |
Logical. If |
Named numeric vector of standard deviations.
From a combined bundle's per-replicate results (the recorder columns of
the fb_mr_field template family: nv_H_*, mr_H_*, fld_H_*,
betaHhat_H, detected, and their Hc twins), computes, per estimator
over the detected replicates: the retained bias on the log scale, the
empirical SD of the log estimate, the mean reported SE, their ratios
b = bias_log / sd_emp and r = se_mean / sd_emp, the observed one- and
two-sided coverage of the target with Wilson intervals, and the Gaussian
reference coverages at (b, r).
fs_sim_bias_coverage( results, block = c("H", "Hc"), estimators = c("naive", "mr", "fld"), level = 0.95, target = "betaHhat", side = c("lower", "upper"), scale = c("log", "identity") )fs_sim_bias_coverage( results, block = c("H", "Hc"), estimators = c("naive", "mr", "fld"), level = 0.95, target = "betaHhat", side = c("lower", "upper"), scale = c("log", "identity") )
results |
Data frame of per-replicate records from a combined bundle
( |
block |
|
estimators |
Subset of |
level |
Coverage level (default 0.95). |
target |
|
side |
|
scale |
|
Conventions: the
one-sided lower bound is the stored fld_<block>_lo1s for the field row
and exp(log est - qnorm(level) * SE) for naive and MR (IJ); two-sided
coverage uses each estimator's stored bounds; the field estimator is the
shrunk-field est2 with the two-sided Lambda-quantile interval. Under
side = "upper" (the complement's benefit-claim orientation) the one-sided coverage is
truth <= upper bound, with the field's stored fld_<block>_up1s and
exp(log est + qnorm(level) * SE) for naive and MR (IJ), and the Gaussian
reference is Phi(z_level * r + b) – a positive retained bias helps an
upper bound where it hurts a lower one.
Data frame, one row per estimator: estimator, n, bias_log,
sd_emp, se_mean, b, r, cov1, cov1_wilson_lo,
cov1_wilson_hi, cov2, cov2_wilson_lo, cov2_wilson_hi,
cov1_ref, cov2_ref.
Computes the symmetric matrix square root of scale * S after
symmetrising S as (S + t(S)) / 2: with eigendecomposition
, the returned matrix is (eigenvalues
scaled by scale and clamped at zero before the square root), so that
.
fs_sym_root(S, scale = 2)fs_sym_root(S, scale = 2)
S |
Numeric square matrix, a covariance (or any matrix whose
symmetrisation |
scale |
Numeric scalar multiplying |
A symmetric numeric matrix R of the same dimension as
S, satisfying R %*% t(R) equal to scale times the
symmetrised S (up to the zero-clamp on negative eigenvalues).
Both roots satisfy , but the asymmetric
root depends on the eigenvector basis, which is not a
continuous function of its input: signs flip and vectors rotate freely
inside degenerate subspaces – which a candidate covariance built from
complement pairs has exactly, since the complement constraints make it
rank-deficient by construction. A pinned RNG seed fixes the standard-normal
draws Z; it cannot fix the basis Z is multiplied by, so the asymmetric root
is non-reproducible across arithmetically equivalent routes to the same
matrix. The symmetric root is a matrix function of the
symmetrised input alone – basis-free and continuous – so samples built
from it are continuous, reproducible functions of the inputs.
S <- crossprod(matrix(rnorm(25), 5)) # symmetric positive definite R <- fs_sym_root(S, scale = 2) max(abs(R %*% t(R) - 2 * S)) # ~ 1e-14 isSymmetric(R) # Rank-deficient input: the root exists and stays symmetric X <- matrix(rnorm(15), 5, 3) R2 <- fs_sym_root(tcrossprod(X), scale = 1) max(abs(R2 %*% t(R2) - tcrossprod(X)))S <- crossprod(matrix(rnorm(25), 5)) # symmetric positive definite R <- fs_sym_root(S, scale = 2) max(abs(R %*% t(R) - 2 * S)) # ~ 1e-14 isSymmetric(R) # Rank-deficient input: the root exists and stays symmetric X <- matrix(rnorm(15), 5, 3) R2 <- fs_sym_root(tcrossprod(X), scale = 1) max(abs(R2 %*% t(R2) - tcrossprod(X)))
Bridge from a forestsearch(sg_focus = "maxeff") fit to the Guo and He
(2021) Algorithm 3 de-biasing estimator guohe_algorithm3(). This is the
"post-hoc identified subgroups" application of Guo and He: forestsearch
enumerates a large candidate family and selects the argmax; Guo and He
supplies a de-biased effect and one-sided bound for the selected subgroup,
correcting the winner's-curse optimism of that selection. The candidate
family is materialized from the fit with fs_export_maxeff_family() and,
by default, verified against forestsearch's own selected-subgroup
membership with fs_assert_membership() before any de-biasing runs.
fs_to_guohe( fit, time = "time_months", event = "status", treatment = "hormon", B = 2000L, r = 0.03, level = 0.05, seed = NULL, min_events = 5L, parallel = FALSE, verify = TRUE )fs_to_guohe( fit, time = "time_months", event = "status", treatment = "hormon", B = 2000L, r = 0.03, level = 0.05, seed = NULL, min_events = 5L, parallel = FALSE, verify = TRUE )
fit |
A |
time, event, treatment
|
Names of the time, event, and 0/1 treatment
columns on |
B |
Number of bootstrap resamples passed to |
r |
Shrinkage tuning parameter, strictly between 0 and 0.5; see
|
level |
One-sided level; the two-sided interval has coverage
|
seed |
Optional integer for reproducible resampling. |
min_events |
Minimum events for a candidate to be estimable. |
parallel |
Logical; passed to |
verify |
Logical; if |
The de-biasing engine is an independent implementation of the published method, validated by reproducing the simulation study of Section 5 of Guo and He (2021).
The Guo and He selection is orient = +1 (the maximum-hazard-ratio, i.e.
harm, subgroup), so its argmax coincides with the forestsearch maxeff
selection. If the two nevertheless disagree (possible when candidates are
dropped as non-estimable), a warning is issued and the returned Guo and He
objects refer to the Guo and He argmax.
A list with elements gh (the full "guohe_a3" object), export
(the fs_export_maxeff_family() result), selected (the Guo and He
argmax candidate name), and the convenience scalars naive_hr,
debiased_hr, and bound_hr (hazard-ratio scale).
Guo, X. and He, X. (2021). Inference on Selected Subgroups in Clinical Trials. Journal of the American Statistical Association, 116(535), 1498–1506. doi:10.1080/01621459.2020.1740096
fs_export_maxeff_family(), fs_assert_membership(),
guohe_algorithm3(), guohe_adaptive_r()
set.seed(11) n <- 200 df <- data.frame(id = seq_len(n), treat = rbinom(n, 1, 0.5), age = round(rnorm(n, 55, 12)), bm = factor(rbinom(n, 1, 0.5))) harm <- df$age <= 50 & df$bm == "1" tt <- rexp(n, 0.05 * exp(log(2.5) * df$treat * harm)) df$time <- pmin(tt, 60) df$event <- as.integer(tt <= 60) fit <- forestsearch( df.analysis = df, outcome.name = "time", event.name = "event", treat.name = "treat", id.name = "id", confounders.name = c("age", "bm"), sg_focus = "maxeff", use_grf = FALSE, use_lasso = FALSE, fs.splits = 50, n.min = 30, d0.min = 5, d1.min = 5, hr.threshold = 1.1, hr.consistency = 1.0, pconsistency.threshold = 0.5, maxk = 2, details = FALSE, plot.sg = FALSE, parallel_args = list(plan = "sequential", workers = 1L, show_message = FALSE) ) gh <- fs_to_guohe(fit, time = "time", event = "event", treatment = "treat", B = 50, seed = 3) gh$naive_hr gh$debiased_hr gh$bound_hrset.seed(11) n <- 200 df <- data.frame(id = seq_len(n), treat = rbinom(n, 1, 0.5), age = round(rnorm(n, 55, 12)), bm = factor(rbinom(n, 1, 0.5))) harm <- df$age <= 50 & df$bm == "1" tt <- rexp(n, 0.05 * exp(log(2.5) * df$treat * harm)) df$time <- pmin(tt, 60) df$event <- as.integer(tt <= 60) fit <- forestsearch( df.analysis = df, outcome.name = "time", event.name = "event", treat.name = "treat", id.name = "id", confounders.name = c("age", "bm"), sg_focus = "maxeff", use_grf = FALSE, use_lasso = FALSE, fs.splits = 50, n.min = 30, d0.min = 5, d1.min = 5, hr.threshold = 1.1, hr.consistency = 1.0, pconsistency.threshold = 0.5, maxk = 2, details = FALSE, plot.sg = FALSE, parallel_args = list(plan = "sequential", workers = 1L, show_message = FALSE) ) gh <- fs_to_guohe(fit, time = "time", event = "event", treatment = "treat", B = 50, seed = 3) gh$naive_hr gh$debiased_hr gh$bound_hr
Creates a data generating mechanism (DGM) for survival data using an Accelerated Failure Time (AFT) model with Weibull distribution. Supports flexible subgroup definitions and treatment-subgroup interactions.
generate_aft_dgm_flex( data, continuous_vars, factor_vars, continuous_vars_cens = NULL, factor_vars_cens = NULL, set_beta_spec = list(set_var = NULL, beta_var = NULL), outcome_var, event_var, treatment_var = NULL, subgroup_vars = NULL, subgroup_cuts = NULL, draw_treatment = FALSE, model = "alt", k_treat = 1, k_inter = 1, n_super = 5000, select_censoring = TRUE, cens_type = "weibull", cens_params = list(), cens_intercept_only = FALSE, seed = 8316951, verbose = TRUE, standardize = FALSE, spline_spec = NULL )generate_aft_dgm_flex( data, continuous_vars, factor_vars, continuous_vars_cens = NULL, factor_vars_cens = NULL, set_beta_spec = list(set_var = NULL, beta_var = NULL), outcome_var, event_var, treatment_var = NULL, subgroup_vars = NULL, subgroup_cuts = NULL, draw_treatment = FALSE, model = "alt", k_treat = 1, k_inter = 1, n_super = 5000, select_censoring = TRUE, cens_type = "weibull", cens_params = list(), cens_intercept_only = FALSE, seed = 8316951, verbose = TRUE, standardize = FALSE, spline_spec = NULL )
data |
A data.frame containing the input dataset to base the simulation on |
continuous_vars |
Character vector of continuous variable names to be standardized and included as covariates |
factor_vars |
Character vector of factor/categorical variable names to be converted to dummy variables (largest value as reference) |
continuous_vars_cens |
Character vector of continuous variable names to be used for censoring model. If NULL, uses same as continuous_vars. Default NULL |
factor_vars_cens |
Character vector of factor variable names to be used for censoring model. If NULL, uses same as factor_vars. Default NULL |
set_beta_spec |
List with elements 'set_var' and 'beta_var' for manually setting specific beta coefficients. Default list(set_var = NULL, beta_var = NULL) |
outcome_var |
Character string specifying the name of the outcome/time variable |
event_var |
Character string specifying the name of the event/status variable (1 = event, 0 = censored) |
treatment_var |
Character string specifying the name of the treatment variable. If NULL, treatment will be randomly simulated with 50/50 allocation |
subgroup_vars |
Character vector of variable names defining the subgroup. Default is NULL (no subgroups) |
subgroup_cuts |
Named list of cutpoint specifications for subgroup variables. See Details section for flexible specification options |
draw_treatment |
Logical indicating whether to redraw treatment assignment in simulation. Default is FALSE (use original assignments) |
model |
Character string: "alt" for alternative model with subgroup effects, "null" for null model without subgroup effects. Default is "alt" |
k_treat |
Numeric treatment effect modifier. Values >1 increase treatment effect, <1 decrease it. Default is 1 (no modification) |
k_inter |
Numeric interaction effect modifier for treatment-subgroup interaction. Default is 1 (no modification) |
n_super |
Integer specifying size of super population to generate. Default is 5000 |
select_censoring |
Logical. If |
cens_type |
Character string specifying censoring distribution type:
|
cens_params |
Named list of censoring distribution parameters.
Interpretation depends on
Default |
cens_intercept_only |
Logical. Honored only in force-fit mode
(
Setting |
seed |
Integer random seed for reproducibility. Default is 8316951 |
verbose |
Logical indicating whether to print diagnostic information during execution. Default is TRUE |
standardize |
Logical indicating whether to standardize continuous variables. Default is FALSE |
spline_spec |
List specifying spline configuration for treatment effect. Must include 'var' (variable name), 'knot', 'zeta', and 'log_hrs' (vector of length 3). Default NULL (no spline) |
The subgroup_cuts parameter accepts multiple flexible specifications:
subgroup_cuts = list(er = 20) # er <= 20
subgroup_cuts = list( er = list(type = "quantile", value = 0.25) # er <= 25th percentile )
subgroup_cuts = list( er = list(type = "function", fun = median) # er <= median )
subgroup_cuts = list( age = list(type = "range", min = 40, max = 60) # 40 <= age <= 60 )
subgroup_cuts = list( nodes = list(type = "greater", quantile = 0.75) # nodes > 75th percentile )
subgroup_cuts = list( grade = list(type = "multiple", values = c(2, 3)) # grade in (2, 3) )
subgroup_cuts = list(
er = list(
type = "custom",
fun = function(x) x <= quantile(x, 0.3) | x >= quantile(x, 0.9)
)
)
The AFT model with Weibull distribution is specified as:
Where:
is the survival time
is the intercept
contains the covariate effects
includes treatment, covariates, and treatment x subgroup interaction
is the scale parameter
follows an extreme value distribution
The model creates a SINGLE interaction term representing the treatment effect modification when ALL subgroup conditions are simultaneously satisfied. This is not multiple separate interactions but one combined indicator.
An object of class "aft_dgm_flex" (a list) with components:
df_superData frame containing the super-population,
including covariates, treatment, counterfactual linear predictors,
and subgroup indicator flag_harm.
df_sourceData frame with every source patient exactly
once (the prepared trial data), carrying the same counterfactual
outcome and censoring linear predictors as df_super.
Consumed by simulate_from_dgm(baseline = "fixed") to hold
baseline covariates – and hence subgroup composition – fixed
across simulated trials.
model_paramsList with AFT parameters mu,
tau, gamma, b0, the fitted censoring
model, and optional spline_info.
subgroup_infoList describing the true subgroup:
vars, cuts, definitions, size,
proportion.
hazard_ratiosList of true HR/AHR/CDE values on the
super-population (see compute_dgm_cde).
analysis_varsNamed list of column roles (continuous, factor, covariates, treatment, outcome, event).
model_type"null" or "alt".
n_superSuper-population size.
seedSeed used for super-population generation.
Your Name
Leon, L.F., et al. (2024). Statistics in Medicine.
Kalbfleisch, J.D. and Prentice, R.L. (2002). The Statistical Analysis of Failure Time Data (2nd ed.). Wiley.
## Not run: df <- survival::gbsg dgm <- generate_aft_dgm_flex( data = df, outcome_var = "rfstime", event_var = "status", treatment_var = "hormon", continuous_vars = c("age", "size", "nodes", "pgr", "er"), factor_vars = "meno", model = "null", verbose = FALSE ) str(dgm) ## End(Not run)## Not run: df <- survival::gbsg dgm <- generate_aft_dgm_flex( data = df, outcome_var = "rfstime", event_var = "status", treatment_var = "hormon", continuous_vars = c("age", "size", "nodes", "pgr", "er"), factor_vars = "meno", model = "null", verbose = FALSE ) str(dgm) ## End(Not run)
Generate Synthetic Data using Bootstrap with Perturbation
generate_bootstrap_synthetic( data, continuous_vars, cat_vars, n = NULL, seed = 123, noise_level = 0.1, id_var = NULL, cat_flip_prob = NULL, preserve_bounds = TRUE, ordinal_vars = NULL )generate_bootstrap_synthetic( data, continuous_vars, cat_vars, n = NULL, seed = 123, noise_level = 0.1, id_var = NULL, cat_flip_prob = NULL, preserve_bounds = TRUE, ordinal_vars = NULL )
data |
Original dataset to bootstrap from |
continuous_vars |
Character vector of continuous variable names |
cat_vars |
Character vector of categorical variable names |
n |
Number of synthetic observations to generate (default: same as original) |
seed |
Random seed for reproducibility |
noise_level |
Noise level for perturbation (0 to 1, default 0.1) |
id_var |
Optional name of ID variable to regenerate (will be numbered 1:n) |
cat_flip_prob |
Probability of flipping categorical values (default: noise_level/2) |
preserve_bounds |
Logical: should continuous variables stay within original bounds? (default: TRUE) |
ordinal_vars |
Optional character vector of ordinal categorical variables (these will be perturbed to adjacent values rather than randomly flipped) |
A data frame with synthetic data
# Example 1: Using with GBSG dataset synth_gbsg <- generate_bootstrap_synthetic( data = survival::gbsg, continuous_vars = c("age", "size", "nodes", "pgr", "er", "rfstime"), cat_vars = c("meno", "hormon", "status"), ordinal_vars = c("grade"), id_var = "pid", n = 1000, seed = 123, noise_level = 0.15 ) # Example 2: Using with any dataset my_data <- data.frame( id = 1:100, height = rnorm(100, 170, 10), weight = rnorm(100, 70, 15), age = sample(20:80, 100, replace = TRUE), gender = sample(c("M", "F"), 100, replace = TRUE), education = sample(1:5, 100, replace = TRUE), smoker = sample(0:1, 100, replace = TRUE) ) synth_data <- generate_bootstrap_synthetic( data = my_data, continuous_vars = c("height", "weight", "age"), cat_vars = c("gender", "smoker"), ordinal_vars = c("education"), id_var = "id", n = 150, seed = 456 )# Example 1: Using with GBSG dataset synth_gbsg <- generate_bootstrap_synthetic( data = survival::gbsg, continuous_vars = c("age", "size", "nodes", "pgr", "er", "rfstime"), cat_vars = c("meno", "hormon", "status"), ordinal_vars = c("grade"), id_var = "pid", n = 1000, seed = 123, noise_level = 0.15 ) # Example 2: Using with any dataset my_data <- data.frame( id = 1:100, height = rnorm(100, 170, 10), weight = rnorm(100, 70, 15), age = sample(20:80, 100, replace = TRUE), gender = sample(c("M", "F"), 100, replace = TRUE), education = sample(1:5, 100, replace = TRUE), smoker = sample(0:1, 100, replace = TRUE) ) synth_data <- generate_bootstrap_synthetic( data = my_data, continuous_vars = c("height", "weight", "age"), cat_vars = c("gender", "smoker"), ordinal_vars = c("education"), id_var = "id", n = 150, seed = 456 )
Creates a bootstrap sample from a dataset with controlled noise added to both continuous and categorical variables. This function is useful for generating synthetic datasets that maintain the general structure of the original data while introducing controlled variation.
generate_bootstrap_with_noise( data, n = NULL, continuous_vars = NULL, cat_vars = NULL, id_var = "pid", seed = 123, noise_level = 0.1 )generate_bootstrap_with_noise( data, n = NULL, continuous_vars = NULL, cat_vars = NULL, id_var = "pid", seed = 123, noise_level = 0.1 )
data |
A data frame containing the original dataset to bootstrap from. |
n |
Integer. Number of observations in the output dataset. If NULL (default), uses the same number of rows as the input data. |
continuous_vars |
Character vector of column names to treat as continuous variables. If NULL (default), automatically detects numeric columns. |
cat_vars |
Character vector of column names to treat as categorical variables. If NULL (default), automatically detects factors, logical columns, and numeric columns with 10 or fewer unique values. |
id_var |
Character string specifying the name of the ID variable column. This column will be reset to sequential values (1:n) in the output. Default is "pid". |
seed |
Integer. Random seed for reproducibility. Default is 123. |
noise_level |
Numeric between 0 and 1. Controls the amount of noise added. For continuous variables, this is multiplied by the standard deviation to determine noise magnitude. For categorical variables, this is divided by 2 to determine the probability of value changes. Default is 0.1. |
The function performs the following operations:
Samples n observations with replacement from the original dataset.
Adds Gaussian noise with standard deviation = original SD × noise_level
Constrains values to remain within original variable bounds
Preserves integer type for variables that appear to be integers
Changes values with probability = noise_level / 2
Binary variables: flips to opposite value
Multi-level unordered: randomly selects from other levels
Ordered factors: weights selection toward adjacent levels
Preserves factor levels and ordering from original data
A data frame with the same structure as the input data, containing bootstrap sampled observations with added noise.
The function assumes that categorical variables with numeric encoding should maintain their numeric type unless they are factors in the input
Missing values (NA) are handled appropriately in calculations but are not imputed
For ordered factors or variables named "grade", the perturbation favors transitions to adjacent levels over distant levels
sample for bootstrap sampling,
rnorm for noise generation
## Not run: # Load example dataset data(gbsg, package = "survival") # Basic usage with automatic variable detection synthetic_data <- generate_bootstrap_with_noise( data = gbsg, seed = 123 ) # Specify variables explicitly synthetic_data <- generate_bootstrap_with_noise( data = gbsg, n = 1000, continuous_vars = c("age", "size", "nodes", "pgr", "er", "rfstime"), cat_vars = c("meno", "grade", "hormon", "status"), id_var = "pid", seed = 456, noise_level = 0.15 ) # Create multiple synthetic datasets synthetic_list <- lapply(1:10, function(i) { generate_bootstrap_with_noise(data = gbsg, seed = i) }) ## End(Not run)## Not run: # Load example dataset data(gbsg, package = "survival") # Basic usage with automatic variable detection synthetic_data <- generate_bootstrap_with_noise( data = gbsg, seed = 123 ) # Specify variables explicitly synthetic_data <- generate_bootstrap_with_noise( data = gbsg, n = 1000, continuous_vars = c("age", "size", "nodes", "pgr", "er", "rfstime"), cat_vars = c("meno", "grade", "hormon", "status"), id_var = "pid", seed = 456, noise_level = 0.15 ) # Create multiple synthetic datasets synthetic_list <- lapply(1:10, function(i) { generate_bootstrap_with_noise(data = gbsg, seed = i) }) ## End(Not run)
Computes detection probability across a range of hazard ratios to create a power-like curve for subgroup detection.
generate_detection_curve( theta_range = c(0.5, 3), n_points = 50L, n_sg, prop_cens = 0.3, hr_threshold = 1.25, hr_consistency = 1, include_reference = TRUE, method = "cubature", verbose = TRUE )generate_detection_curve( theta_range = c(0.5, 3), n_points = 50L, n_sg, prop_cens = 0.3, hr_threshold = 1.25, hr_consistency = 1, include_reference = TRUE, method = "cubature", verbose = TRUE )
theta_range |
Numeric vector of length 2. Range of HR values to evaluate. Default: c(0.5, 3.0) |
n_points |
Integer. Number of points to evaluate. Default: 50 |
n_sg |
Integer. Subgroup sample size. |
prop_cens |
Numeric. Proportion censored (0-1). Default: 0.3 |
hr_threshold |
Numeric. HR threshold for detection. Default: 1.25 |
hr_consistency |
Numeric. HR consistency threshold. Default: 1.0 |
include_reference |
Logical. Include reference HR values (0.5, 0.75, 1.0). Default: TRUE |
method |
Character. Integration method. Default: "cubature" |
verbose |
Logical. Print progress. Default: TRUE |
A data.frame with columns:
theta |
Hazard ratio values |
probability |
Detection probability |
n_sg |
Subgroup size (repeated) |
prop_cens |
Censoring proportion (repeated) |
hr_threshold |
Detection threshold (repeated) |
## Not run: # Generate detection curve curve_data <- generate_detection_curve( n_sg = 60, prop_cens = 0.2, hr_threshold = 1.25 ) # Plot plot_detection_curve(curve_data) ## End(Not run)## Not run: # Generate detection curve curve_data <- generate_detection_curve( n_sg = 60, prop_cens = 0.2, hr_threshold = 1.25 ) # Plot plot_detection_curve(curve_data) ## End(Not run)
Generate Synthetic GBSG Data using Generalized Bootstrap
generate_gbsg_bootstrap_general(n = 686, seed = 123, noise_level = 0.1)generate_gbsg_bootstrap_general(n = 686, seed = 123, noise_level = 0.1)
n |
Number of observations |
seed |
Random seed |
noise_level |
Noise level for perturbation |
Synthetic GBSG dataset
## Not run: df_synth <- generate_gbsg_bootstrap_general(n = 200, seed = 42) dim(df_synth) names(df_synth) ## End(Not run)## Not run: df_synth <- generate_gbsg_bootstrap_general(n = 200, seed = 42) dim(df_synth) names(df_synth) ## End(Not run)
Creates a data generating mechanism (DGM) for non-survival outcomes using generalised linear models.
generate_glm_dgm( data, factor_vars, continuous_vars = NULL, outcome_var, treatment_var, outcome_type = c("binary", "continuous", "count"), effect_measure = NULL, offset_var = NULL, subgroup_vars = NULL, subgroup_cuts = NULL, model = c("alt", "null"), k_treat = 1, k_inter = 0, adverse_outcome = FALSE, n_super = 5000L, seed = 8316951L, verbose = FALSE, k_random_noise = 0L, noise_seed = 20260807L )generate_glm_dgm( data, factor_vars, continuous_vars = NULL, outcome_var, treatment_var, outcome_type = c("binary", "continuous", "count"), effect_measure = NULL, offset_var = NULL, subgroup_vars = NULL, subgroup_cuts = NULL, model = c("alt", "null"), k_treat = 1, k_inter = 0, adverse_outcome = FALSE, n_super = 5000L, seed = 8316951L, verbose = FALSE, k_random_noise = 0L, noise_seed = 20260807L )
data |
A data frame containing the source dataset. |
factor_vars |
Character vector of factor/categorical variable names
to include as covariates. These are the candidate confounders for
ForestSearch (e.g., |
continuous_vars |
Character vector of continuous variable names
to include as prognostic covariates in the baseline GLM. These
improve the model fit but do not participate in subgroup definition.
Consistent with |
outcome_var |
Character string naming the outcome variable. |
treatment_var |
Character string naming the treatment variable (must be 0/1 coded). |
outcome_type |
Character. One of |
effect_measure |
Character. Effect measure for the GLM.
Default is |
offset_var |
Character or |
subgroup_vars |
Character vector of variable names defining the
subgroup. Default |
subgroup_cuts |
Named list of cutpoint specifications for
subgroup variables. Uses the same flexible format as
|
model |
Character. |
k_treat |
Numeric. Scaling factor for the fitted treatment
coefficient on the linear predictor scale. |
k_inter |
Numeric. Direct additive shift on the linear predictor
for Q members under treatment. The interpretation depends on
Default |
adverse_outcome |
Logical. If |
n_super |
Integer. Size of the super-population. Default
|
seed |
Integer. Random seed for super-population sampling.
Default |
verbose |
Logical. Print diagnostic information. Default
|
k_random_noise |
Integer. Number of standard-normal noise columns
( |
noise_seed |
Integer. Seed for the population-level noise draw, used
only when |
Fits a GLM to the source data, creates a super-population with
individual-level potential outcomes, and stores true subgroup effects.
The returned object can be passed to simulate_from_glm_dgm
for simulation studies, and is compatible with
run_simulation_analysis (which dispatches on class).
Noise columns requested via k_random_noise are inert
N(0,1) population attributes intended as candidate confounders; the
builder never puts them in the outcome model. They are drawn once onto
df_super after sampling, so simulated trials inherit them by
row resampling.
An object of class c("glm_dgm", "list") with:
df_superSuper-population data frame with potential
outcome columns: p0, p1 (binary probabilities),
or mu0, mu1 (conditional means for continuous /
count outcomes), and flag_harm.
df_sourceThe observed analysis frame in observed
order (every source patient exactly once), carrying the same
potential-outcome columns as df_super computed by the
identical prediction calls, plus flag_harm and the
discretised covariates. This is the frozen panel consumed by
simulate_from_glm_dgm(baseline = "fixed").
hazard_ratiosNamed list with harm_subgroup,
no_harm_subgroup, and overall – effect estimates
on the scale determined by effect_measure. Field name
retained for compatibility with get_dgm_hr() and
reporting functions.
outcome_typeCharacter: "binary",
"continuous", or "count".
effect_measureCharacter: the effect measure used.
model_paramsList with fitted coefficients, family,
k_inter, residual SD (continuous), and offset variable
name (count).
subgroup_infoList with subgroup definition, size, and proportion.
model_typeCharacter: "alt" or "null".
noise_namesCharacter vector of noise column names
(character(0) when k_random_noise = 0).
noise_scheme"population" when noise was drawn,
otherwise "none".
noise_seedThe seed used for the noise draw, or
NA_integer_ when none was drawn.
simulate_from_glm_dgm,
calibrate_glm_interaction,
generate_aft_dgm_flex
## Not run: library(speff2trial) actg <- subset(ACTG175, arms %in% c(1, 3)) actg$treat <- 1L - as.integer(actg$arms == 1) actg$y <- as.integer(actg$cd420 > actg$cd40) actg$z1 <- as.factor(ifelse(actg$age > 34, 1L, 0L)) actg$z2 <- as.factor(ifelse(actg$preanti <= 745, 1L, 0L)) dgm <- generate_glm_dgm( data = actg, factor_vars = c("z1", "z2"), outcome_var = "y", treatment_var = "treat", outcome_type = "binary", subgroup_vars = c("z1", "z2"), subgroup_cuts = list(z1 = 1L, z2 = 1L), model = "alt", k_inter = 1.0, # additive log-odds shift for Q under treatment verbose = TRUE ) ## End(Not run)## Not run: library(speff2trial) actg <- subset(ACTG175, arms %in% c(1, 3)) actg$treat <- 1L - as.integer(actg$arms == 1) actg$y <- as.integer(actg$cd420 > actg$cd40) actg$z1 <- as.factor(ifelse(actg$age > 34, 1L, 0L)) actg$z2 <- as.factor(ifelse(actg$preanti <= 745, 1L, 0L)) dgm <- generate_glm_dgm( data = actg, factor_vars = c("z1", "z2"), outcome_var = "y", treatment_var = "treat", outcome_type = "binary", subgroup_vars = c("z1", "z2"), subgroup_cuts = list(z1 = 1L, z2 = 1L), model = "alt", k_inter = 1.0, # additive log-odds shift for Q under treatment verbose = TRUE ) ## End(Not run)
Generates all possible combinations of subgroup factors up to a specified maximum size.
get_combinations_info(L, maxk)get_combinations_info(L, maxk)
L |
Integer. Number of subgroup factors. |
maxk |
Integer. Maximum number of factors in a combination. |
List with max_count (total combinations) and indices_list (indices for each k).
# L=6 single-factor subgroups, combinations up to 2 factors info <- get_combinations_info(L = 6, maxk = 2) info$max_count # total number of combinations length(info$indices_list)# L=6 single-factor subgroups, combinations up to 2 factors info <- get_combinations_info(L = 6, maxk = 2) info$max_count # total number of combinations length(info$indices_list)
For each variable in conf.force.names, returns cut expressions if continuous.
get_conf_force(df, conf.force.names, cont.cutoff = 4)get_conf_force(df, conf.force.names, cont.cutoff = 4)
df |
Data frame. |
conf.force.names |
Character vector of variable names. |
cont.cutoff |
Integer. Cutoff for continuous. |
Character vector of cut expressions.
df <- data.frame(age = c(45, 60, 35), size = c(20, 35, 15), grade = c(1, 2, 3)) get_conf_force(df, conf.force.names = c("age", "size"))df <- data.frame(age = c(45, 60, 35), size = c(20, 35, 15), grade = c(1, 2, 3)) get_conf_force(df, conf.force.names = c("age", "size"))
For each named variable in conf_jcuts, returns J
deferred cut expressions of the form "X <= qj(X, k, J + 1)"
via cut_var_jq(). Variables that are not continuous (per
cont.cutoff) are skipped with a warning. This is the
cut_var_jq/J-quantile analog of get_conf_force().
get_conf_force_jq(df, conf_jcuts, cont.cutoff = 4)get_conf_force_jq(df, conf_jcuts, cont.cutoff = 4)
df |
Data frame. |
conf_jcuts |
Named list of positive integers. Each name is a
continuous-variable column in |
cont.cutoff |
Integer. Cutoff for continuous determination
(passed through to |
Conflicts with other forced-cut mechanisms (defaultcut_names,
conf.cont_medians_force) are validated upstream by
get_FSdata(); this helper performs only structural checks.
Character vector of cut expressions (length sum(J_v)
over continuous v), or character(0) if none.
df <- data.frame(age = c(45, 60, 35, 50, 70, 25, 80, 55)) # 5 binary cuts at the 1/6, 2/6, ..., 5/6 quantiles of age: get_conf_force_jq(df, conf_jcuts = list(age = 5))df <- data.frame(age = c(45, 60, 35, 50, 70, 25, 80, 55)) # 5 binary cuts at the 1/6, 2/6, ..., 5/6 quantiles of age: get_conf_force_jq(df, conf_jcuts = list(age = 5))
Returns a vector indicating which factors are included in a subgroup combination.
get_covs_in( kk, maxk, L, counts_1factor, index_1factor, counts_2factor = NULL, index_2factor = NULL, counts_3factor = NULL, index_3factor = NULL )get_covs_in( kk, maxk, L, counts_1factor, index_1factor, counts_2factor = NULL, index_2factor = NULL, counts_3factor = NULL, index_3factor = NULL )
kk |
Integer. Index of the combination. |
maxk |
Integer. Maximum number of factors in a combination. |
L |
Integer. Number of subgroup factors. |
counts_1factor |
Integer. Number of single-factor combinations. |
index_1factor |
Matrix of indices for single-factor combinations. |
counts_2factor |
Integer. Number of two-factor combinations. |
index_2factor |
Matrix of indices for two-factor combinations. |
counts_3factor |
Integer. Number of three-factor combinations. |
index_3factor |
Matrix of indices for three-factor combinations. |
Numeric vector indicating selected factors (1 = included, 0 = not included).
# Build index matrices directly (L=4, maxk=2) L <- 4 idx1 <- matrix(seq_len(L), ncol = 1) # single-factor indices idx2 <- t(utils::combn(L, 2)) # two-factor indices # Combination 3 falls in the single-factor block (kk=3 -> factor 3) get_covs_in(kk = 3, maxk = 2, L = L, counts_1factor = nrow(idx1), index_1factor = idx1, counts_2factor = nrow(idx2), index_2factor = idx2)# Build index matrices directly (L=4, maxk=2) L <- 4 idx1 <- matrix(seq_len(L), ncol = 1) # single-factor indices idx2 <- t(utils::combn(L, 2)) # two-factor indices # Combination 3 falls in the single-factor block (kk=3 -> factor 3) get_covs_in(kk = 3, maxk = 2, L = L, counts_1factor = nrow(idx1), index_1factor = idx1, counts_2factor = nrow(idx2), index_2factor = idx2)
Fits a Cox model for a subgroup and returns estimate and standard error.
get_Cox_sg( df_sg, cox.formula, est.loghr = TRUE, cox_initial = log(1), treat.name = NULL )get_Cox_sg( df_sg, cox.formula, est.loghr = TRUE, cox_initial = log(1), treat.name = NULL )
df_sg |
Data frame for subgroup. |
cox.formula |
Cox model formula. |
est.loghr |
Logical. Is estimate on log(HR) scale? |
cox_initial |
Optional pre-fitted Cox model object to use instead of fitting a new model. Default NULL |
treat.name |
Character or |
Function is utilized throughout codebase
List with estimate and standard error.
## Not run: library(survival) df <- data.frame( tte = gbsg$rfstime / 30.4375, event = gbsg$status, treat = gbsg$hormon ) formula <- build_cox_formula("tte", "event", "treat") result <- get_Cox_sg(df, cox.formula = formula) exp(result$est_obs) # hazard ratio ## End(Not run)## Not run: library(survival) df <- data.frame( tte = gbsg$rfstime / 30.4375, event = gbsg$status, treat = gbsg$hormon ) formula <- build_cox_formula("tte", "event", "treat") result <- get_Cox_sg(df, cox.formula = formula) exp(result$est_obs) # hazard ratio ## End(Not run)
Creates a prediction dataset with a treatment recommendation flag based
on the subgroup definition. Supports both label expressions
(e.g., "\{er <= 0\}") and bare column names (e.g., "q3.1").
get_dfpred(df.predict, sg.harm, version = 1)get_dfpred(df.predict, sg.harm, version = 1)
df.predict |
Data frame for prediction (test or validation set). |
sg.harm |
Character vector of subgroup-defining labels. Values may
be wrapped in braces and optionally negated, e.g. |
version |
Integer; encoding version (maintained for backward compatibility). Default: 1. |
Each element of sg.harm is processed as follows:
Outer braces and leading ! are stripped.
If the result matches "var op value" (where op is
one of <=, <, >=, >, ==,
!=), the comparison is executed directly on
df.predict[[var]].
Otherwise the expression is treated as a column name and
membership is df.predict[[name]] == 1.
Data frame with treatment recommendation flag
(treat.recommend): 0 for harm subgroup, 1 for complement.
evaluate_comparison for the operator-dispatch
logic, forestsearch for the main analysis function.
## Not run: # With brace-wrapped label expressions sg <- c("{er <= 0}", "{size <= 35}") df_out <- get_dfpred(df.predict = test_data, sg.harm = sg) # With negation sg_neg <- c("{er <= 0}", "!{size <= 35}") df_neg <- get_dfpred(df.predict = test_data, sg.harm = sg_neg) # With bare column names (binary indicators) sg_col <- c("q1.1", "q3.1") df_col <- get_dfpred(df.predict = encoded_data, sg.harm = sg_col) ## End(Not run)## Not run: # With brace-wrapped label expressions sg <- c("{er <= 0}", "{size <= 35}") df_out <- get_dfpred(df.predict = test_data, sg.harm = sg) # With negation sg_neg <- c("{er <= 0}", "!{size <= 35}") df_neg <- get_dfpred(df.predict = test_data, sg.harm = sg_neg) # With bare column names (binary indicators) sg_col <- c("q1.1", "q3.1") df_col <- get_dfpred(df.predict = encoded_data, sg.harm = sg_col) ## End(Not run)
Calculates confidence intervals and bias-corrected estimates for bootstrap results.
get_dfRes( Hobs, seHobs, H1_adj, H2_adj = NULL, ystar, cov_method = "standard", cov_trim = 0, est.scale = "hr", est.loghr = TRUE )get_dfRes( Hobs, seHobs, H1_adj, H2_adj = NULL, ystar, cov_method = "standard", cov_trim = 0, est.scale = "hr", est.loghr = TRUE )
Hobs |
Numeric. Observed estimate. |
seHobs |
Numeric. Standard error of observed estimate. |
H1_adj |
Numeric. Bias-corrected estimate 1. |
H2_adj |
Numeric. Bias-corrected estimate 2 (optional). |
ystar |
Matrix of bootstrap samples. |
cov_method |
Character. Covariance method ("standard" or "nocorrect"). |
cov_trim |
Numeric. Trimming proportion for covariance (default: 0.0). |
est.scale |
Character. "hr" or "1/hr". |
est.loghr |
Logical. Is estimate on log(HR) scale? |
Data.table with confidence intervals and estimates.
## Not run: # get_dfRes is called internally by forestsearch_bootstrap_dofuture(). # To use directly, supply bootstrap sample matrix from bootstrap_ystar(): # ystar <- bootstrap_ystar(df, nb_boots = 100, seed = 42L) # res <- get_dfRes(Hobs = log(1.5), seHobs = 0.3, # H1_adj = 0.05, H2_adj = 0.03, # ystar = ystar) ## End(Not run)## Not run: # get_dfRes is called internally by forestsearch_bootstrap_dofuture(). # To use directly, supply bootstrap sample matrix from bootstrap_ystar(): # ystar <- bootstrap_ystar(df, nb_boots = 100, seed = 42L) # res <- get_dfRes(Hobs = log(1.5), seHobs = 0.3, # H1_adj = 0.05, H2_adj = 0.03, # ystar = ystar) ## End(Not run)
Prepares a dataset for ForestSearch, including options for LASSO-based dimension reduction, GRF cuts, forced cuts, and flexible cut strategies. Returns a list with the processed data, subgroup factor names, cut expressions, and LASSO selection results.
get_FSdata( df.analysis, use_lasso = FALSE, use_grf = FALSE, grf_cuts = NULL, confounders.name, cont.cutoff = 4, conf_force = NULL, conf.cont_medians = NULL, conf.cont_medians_force = NULL, conf.cont_jcuts = NULL, dina_cuts = NULL, collapse_cuts = TRUE, collapse_cuts_args = list(), replace_med_grf = TRUE, defaultcut_names = NULL, cut_type = "default", exclude_cuts = NULL, outcome.name = "tte", event.name = "event", details = TRUE, outcome_type = "survival", offset.name = NULL )get_FSdata( df.analysis, use_lasso = FALSE, use_grf = FALSE, grf_cuts = NULL, confounders.name, cont.cutoff = 4, conf_force = NULL, conf.cont_medians = NULL, conf.cont_medians_force = NULL, conf.cont_jcuts = NULL, dina_cuts = NULL, collapse_cuts = TRUE, collapse_cuts_args = list(), replace_med_grf = TRUE, defaultcut_names = NULL, cut_type = "default", exclude_cuts = NULL, outcome.name = "tte", event.name = "event", details = TRUE, outcome_type = "survival", offset.name = NULL )
df.analysis |
Data frame containing the data. |
use_lasso |
Logical. Whether to use LASSO for dimension reduction. |
use_grf |
Logical. Whether to use GRF cuts. |
grf_cuts |
Character vector of GRF cut expressions. |
confounders.name |
Character vector of confounder variable names. |
cont.cutoff |
Integer. Cutoff for continuous variable determination. |
conf_force |
Character vector of forced cut expressions. |
conf.cont_medians |
Character vector of continuous confounders to cut at median. |
conf.cont_medians_force |
Character vector of additional continuous confounders to force median cut. |
conf.cont_jcuts |
Named list of positive integers, one per
continuous confounder for which J-quantile cuts are desired. For a
variable
Variables not listed here retain default behaviour. J-quantile
cuts are unconditional (not filtered by LASSO), matching
|
dina_cuts |
Character vector of DINA cut expressions (optional),
merged into the candidate pool exactly like |
collapse_cuts |
Logical. If TRUE, collapse near-redundant continuous
candidate cuts after the full pool is assembled and resolved to literal
numerics. Cuts on the same variable and operator whose thresholds lie
within a per-variable standard-error band are merged to a single
rounded-centroid threshold, subject to a membership safety check; see
|
collapse_cuts_args |
List of overrides for the coarsening, merged onto
the defaults |
replace_med_grf |
Logical. If TRUE, removes median cuts that overlap with GRF cuts. |
defaultcut_names |
Character vector of confounders to force default cuts. |
cut_type |
Character. "default" or "median" for cut strategy. |
exclude_cuts |
Character vector of cut expressions to exclude. |
outcome.name |
Character. Name of outcome variable. |
event.name |
Character. Name of event indicator variable. |
details |
Logical. If TRUE, prints details during execution. |
outcome_type |
Character. One of |
offset.name |
Character or |
A list with components:
dfData frame with derived subgroup factor columns.
confs_namesCharacter vector of factor column names
added to df.
confsNamed character vector of cut expressions (continuous cuts plus categorical indicators).
lassokeepCharacter vector of confounders retained
by LASSO screening (empty if use_lasso = FALSE).
lassoomitCharacter vector of confounders dropped by
LASSO screening (empty if use_lasso = FALSE).
library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") fs_data <- get_FSdata(df.analysis = df, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", event.name = "status", use_lasso = FALSE, use_grf = FALSE) names(fs_data)library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") fs_data <- get_FSdata(df.analysis = df, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", event.name = "status", use_lasso = FALSE, use_grf = FALSE) names(fs_data)
Convenience function: loads calibration and computes
.
get_leff(N, outcome_type, output_dir = NULL)get_leff(N, outcome_type, output_dir = NULL)
N |
Integer. Sample size. |
outcome_type |
Character. Outcome type to load. |
output_dir |
Character. Default: NULL (auto). |
Numeric L_eff value.
Fits a minimal Cox model to estimate hazard ratio with reduced overhead. Disables robust variance, model matrix storage, and other extras for speed.
get_split_hr_fast(df, cox_init = 0, adjust_covariates = NULL)get_split_hr_fast(df, cox_init = 0, adjust_covariates = NULL)
df |
data.frame or data.table with Y, Event, Treat columns. |
cox_init |
Numeric. Initial value for coefficient (default 0).
Ignored when |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
Numeric. Estimated hazard ratio for Treat, or NA if model fails.
set.seed(42) df <- data.frame( Y = rexp(80), Event = rbinom(80, 1, 0.6), Treat = rep(0:1, 40) ) get_split_hr_fast(df)set.seed(42) df <- data.frame( Y = rexp(80), Event = rbinom(80, 1, 0.6), Treat = rep(0:1, 40) ) get_split_hr_fast(df)
Returns a vector indicating subgroup membership (1 if all selected factors are present, 0 otherwise).
get_subgroup_membership(zz, covs.in)get_subgroup_membership(zz, covs.in)
zz |
Matrix or data frame of subgroup factor indicators. |
covs.in |
Numeric vector indicating which factors are selected (1 = included). |
Numeric vector of subgroup membership (1/0).
zz <- matrix(c(1, 0, 1, 1, 0, 1), nrow = 3, dimnames = list(NULL, c("er_le0", "grade3"))) get_subgroup_membership(zz, covs.in = c(1, 1)) # both factors required get_subgroup_membership(zz, covs.in = c(1, 0)) # first factor onlyzz <- matrix(c(1, 0, 1, 1, 0, 1), nrow = 3, dimnames = list(NULL, c("er_le0", "grade3"))) get_subgroup_membership(zz, covs.in = c(1, 1)) # both factors required get_subgroup_membership(zz, covs.in = c(1, 0)) # first factor only
Creates a publication-quality forest plot using ggplot2 for the CI panel
and patchwork to assemble label and annotation columns alongside it.
Unlike forestploter, fig.height maps directly to row density —
row_height = fig.height / n_rows with no hidden scaling.
gg_forest( subgroups, est, lo, hi, cat_vec = NULL, cat_colours = NULL, annot = NULL, ref_line = 1, vert_lines = NULL, ref_col = "firebrick", ref_lty = "dashed", vert_col = "grey50", vert_lty = "dotted", xlim = NULL, ticks_at = NULL, tick_labels = NULL, xlog = TRUE, xlab = "Hazard Ratio", title = NULL, subtitle = NULL, footnote = NULL, point_size = 2.5, line_size = 0.8, point_shape = 21, base_size = 11, widths = NULL, row_expand = 0.6, clip_marker = c("none", "arrow") )gg_forest( subgroups, est, lo, hi, cat_vec = NULL, cat_colours = NULL, annot = NULL, ref_line = 1, vert_lines = NULL, ref_col = "firebrick", ref_lty = "dashed", vert_col = "grey50", vert_lty = "dotted", xlim = NULL, ticks_at = NULL, tick_labels = NULL, xlog = TRUE, xlab = "Hazard Ratio", title = NULL, subtitle = NULL, footnote = NULL, point_size = 2.5, line_size = 0.8, point_shape = 21, base_size = 11, widths = NULL, row_expand = 0.6, clip_marker = c("none", "arrow") )
subgroups |
Character vector of subgroup names (displayed top to bottom). |
est |
Numeric vector of point estimates (median HR or similar). |
lo |
Numeric vector of lower bounds (e.g. 1st percentile ECI). |
hi |
Numeric vector of upper bounds (e.g. 99th percentile ECI). |
cat_vec |
Optional character vector of category labels (one per row). Used to colour CI lines and label text. |
cat_colours |
Optional named character vector mapping category labels to colours. Defaults to grey for all rows. |
annot |
Optional named list of character vectors, one per annotation
column. Names become column headers. Each vector must match |
ref_line |
Numeric. X position of the primary reference line (default 1). Drawn as a dashed red line. |
vert_lines |
Numeric vector. X positions of secondary vertical lines (default NULL). Drawn as dotted grey lines. |
ref_col |
Colour of the primary reference line (default "firebrick"). |
ref_lty |
Line type of the primary reference line (default "dashed"). |
vert_col |
Colour of secondary vertical lines (default "grey50"). |
vert_lty |
Line type of secondary vertical lines (default "dotted"). |
xlim |
Numeric vector length 2. X-axis limits for the CI panel. |
ticks_at |
Numeric vector. X-axis tick positions. |
tick_labels |
Character vector. Custom tick labels (default: as.character(ticks_at)). |
xlog |
Logical. If TRUE (default), x-axis on log scale. |
xlab |
Character. X-axis label (default "Hazard Ratio"). |
title |
Character. Overall plot title (default NULL). |
subtitle |
Character. Plot subtitle (default NULL). |
footnote |
Character. Footnote appended below the CI panel (default NULL). |
point_size |
Numeric. Size of point estimate symbol (default 2.5). |
line_size |
Numeric. Line width of CI segments (default 0.8). |
point_shape |
Integer. pch for point estimates (default 21, filled circle). |
base_size |
Numeric. ggplot2 base font size in pt (default 11). Controls all text — increase to make the plot larger; no other knob needed. |
widths |
Numeric vector. Relative patchwork column widths: c(label, ci, annot_1, annot_2, …). Default: c(3.5, 5, rep(1, n_annot)). |
row_expand |
Numeric. Extra space above and below row range on y-axis, in row units (default 0.6). |
clip_marker |
Character. How to mark whiskers whose 1st or 99th
percentile falls outside |
A patchwork object. Render with print() or plot().
Control dimensions entirely via knitr chunk options fig.width /
fig.height: row height = fig.height / n_rows.
## Not run: # Recommended fig.height: n_rows * 0.45 + 1.5 (for title/axis overhead) # e.g. 20 rows -> fig.height = 20 * 0.45 + 1.5 = 10.5 ## End(Not run)## Not run: # Recommended fig.height: n_rows * 0.45 + 1.5 (for title/axis overhead) # e.g. 20 rows -> fig.height = 20 * 0.45 + 1.5 = 10.5 ## End(Not run)
Estimates treatment effects as a function of a continuous covariate using
a generalized linear model with natural cubic splines. This is the GLM
analog of cox_cs_fit for binary, continuous, and count
outcomes.
glm_cs_fit( df, outcome_name = "event", treat_name = "treat", z_name = "bm", family = "binomial", offset_name = NULL, alpha = 0.05, spline_df = 3, z_by = 0.05, z_quantile = 0.95, z_max = Inf, z_window = 0, show_plot = FALSE, plot_params = NULL, truebeta_name = NULL, verbose = TRUE ) ## S3 method for class 'glm_cs_fit' print(x, ...)glm_cs_fit( df, outcome_name = "event", treat_name = "treat", z_name = "bm", family = "binomial", offset_name = NULL, alpha = 0.05, spline_df = 3, z_by = 0.05, z_quantile = 0.95, z_max = Inf, z_window = 0, show_plot = FALSE, plot_params = NULL, truebeta_name = NULL, verbose = TRUE ) ## S3 method for class 'glm_cs_fit' print(x, ...)
df |
Data frame containing outcome data. |
outcome_name |
Character. Name of the outcome variable.
Default: |
treat_name |
Character. Name of the treatment variable (0/1).
Default: |
z_name |
Character. Name of the continuous biomarker covariate.
Default: |
family |
Character or family object. One of |
offset_name |
Character or |
alpha |
Numeric. Significance level for confidence intervals (two-sided). Default: 0.05 (95 percent CI). |
spline_df |
Integer. Degrees of freedom for the natural spline. Default: 3. |
z_by |
Numeric. Increment for the biomarker prediction grid. Default: 0.05. |
z_quantile |
Numeric (0–1). Upper quantile for the prediction grid. Default: 0.95 (5th to 95th percentile). |
z_max |
Numeric. Maximum z value for predictions. Default:
|
z_window |
Numeric. Half-width for counting observations near each grid point. Default: 0.0. |
show_plot |
Logical. Display a base-R diagnostic plot.
Default: |
plot_params |
List. Optional plot parameter overrides (see
|
truebeta_name |
Character or |
verbose |
Logical. Print diagnostic information.
Default: |
x |
A |
... |
Additional arguments (unused). |
For binary outcomes (family = "binomial"):
For count outcomes with offset (family = "poisson"):
For continuous outcomes (family = "gaussian"):
The treatment-effect profile is
estimated on the link scale (log-OR, log-IRR, or identity) with
pointwise delta-method confidence intervals.
A list of class "glm_cs_fit" containing:
Numeric vector. Biomarker grid values.
Numeric vector. Point estimates of the log treatment effect (log-OR, log-IRR, or MD) at each grid point.
Numeric vector. Lower confidence bounds.
Numeric vector. Upper confidence bounds.
Numeric vector. Standard errors (delta method).
Integer vector. Observation counts near each grid point.
Numeric. Overall treatment effect from the no-interaction GLM.
The fitted glm object.
The natural spline basis object.
Character. The GLM family used.
Character. Human-readable label for the effect scale (e.g., "log(OR)", "log(IRR)", "Mean Difference").
Numeric. P-value from the likelihood ratio test comparing the interaction model to the main-effects-only model.
Numeric. Significance level used.
Numeric. Confidence level (1 - alpha).
Invisibly returns x.
cox_cs_fit for the survival analog,
glm_effect_profile for the extended interface with
sandwich SEs and overdispersion correction.
## Not run: # Binary outcome: log-OR profile over a biomarker set.seed(42) df <- data.frame( event = rbinom(500, 1, 0.3), treat = rbinom(500, 1, 0.5), bm = rnorm(500, 0, 1) ) result <- glm_cs_fit(df, outcome_name = "event", z_name = "bm", family = "binomial") # Poisson outcome with offset: log-IRR profile df$ftime <- rexp(500, 0.01) + 1 result_irr <- glm_cs_fit(df, outcome_name = "event", z_name = "bm", family = "poisson", offset_name = "ftime") ## End(Not run)## Not run: # Binary outcome: log-OR profile over a biomarker set.seed(42) df <- data.frame( event = rbinom(500, 1, 0.3), treat = rbinom(500, 1, 0.5), bm = rnorm(500, 0, 1) ) result <- glm_cs_fit(df, outcome_name = "event", z_name = "bm", family = "binomial") # Poisson outcome with offset: log-IRR profile df$ftime <- rexp(500, 0.01) + 1 result_irr <- glm_cs_fit(df, outcome_name = "event", z_name = "bm", family = "poisson", offset_name = "ftime") ## End(Not run)
Estimates a treatment effect profile across a continuous biomarker using a generalized linear model with natural cubic spline interactions. Supports binary, continuous, and count or rate outcomes. Standard errors are computed via the delta method applied to the full model covariance, with optional sandwich variance estimation for modified-Poisson models.
glm_effect_profile( df, outcome.name, treat.name = "treat", z.name = "bm", effect_measure = c("log_OR", "log_RR", "log_IRR", "RD", "MD"), offset.name = NULL, overdispersion = c("none", "quasi", "negbin"), spline_df = 3L, z_by = 0.05, z_quantile = 0.95, alpha = 0.05, conf.level = NULL, strata.name = NULL, show_plot = FALSE, verbose = FALSE )glm_effect_profile( df, outcome.name, treat.name = "treat", z.name = "bm", effect_measure = c("log_OR", "log_RR", "log_IRR", "RD", "MD"), offset.name = NULL, overdispersion = c("none", "quasi", "negbin"), spline_df = 3L, z_by = 0.05, z_quantile = 0.95, alpha = 0.05, conf.level = NULL, strata.name = NULL, show_plot = FALSE, verbose = FALSE )
df |
Data frame containing the analysis data. |
outcome.name |
Character. Name of the outcome variable. Binary outcomes must be coded 0/1; count outcomes must be non-negative integers; continuous outcomes may be any numeric. |
treat.name |
Character. Name of the binary treatment indicator
(0 = control, 1 = treated). Default: |
z.name |
Character. Name of the continuous biomarker (effect modifier).
Default: |
effect_measure |
Character. Effect measure to estimate on the link
scale. Allowed values: |
offset.name |
Character or |
overdispersion |
Character. Overdispersion correction for Poisson
family models. |
spline_df |
Integer. Degrees of freedom for natural spline basis.
Default: |
z_by |
Numeric. Increment for the biomarker prediction grid.
Default: |
z_quantile |
Numeric. Upper quantile of the biomarker used as the
grid endpoint (avoids extrapolation into sparse tails).
Default: |
alpha |
Numeric. Two-sided significance level for confidence
intervals. Default: |
conf.level |
Numeric or |
strata.name |
Character or |
show_plot |
Logical. Display a base-R diagnostic plot of the
estimated treatment effect profile. Default: |
verbose |
Logical. Print diagnostic messages. Default: |
An object of class "glm_effect_profile": a named list
containing z_profile (biomarker grid), est (point
estimates on the link scale), lower and upper
(confidence bounds), se (standard errors),
effect_measure, family_used, overdispersion,
dispersion (quasi or negbin dispersion estimate, else NA),
model_fit, spline_basis, and alpha.
glm_cs_fit for a simpler family-based interface,
cox_cs_fit for survival outcomes,
grf.subg.harm.glm for GRF-based subgroup identification.
Evaluates the performance of GRF-identified subgroups, including hazard ratios, bias, and predictive values. This function is typically used in simulation studies to assess the performance of the GRF subgroup identification method.
grf.subg.eval( df, grf.est, dgm, cox.formula.sim, cox.formula.adj.sim, analysis = "GRF", frac.tau = 1 )grf.subg.eval( df, grf.est, dgm, cox.formula.sim, cox.formula.adj.sim, analysis = "GRF", frac.tau = 1 )
df |
Data frame containing the analysis data. |
grf.est |
List. Output from |
dgm |
List. Data-generating mechanism (truth) for simulation. |
cox.formula.sim |
Formula for unadjusted Cox model. |
cox.formula.adj.sim |
Formula for adjusted Cox model. |
analysis |
Character. Analysis label (default: "GRF"). |
frac.tau |
Numeric. Fraction of tau for GRF horizon (default: 1.0). |
A data frame with evaluation metrics.
## Not run: # grf.subg.eval() is called internally to evaluate GRF subgroup quality. # See grf.subg.harm.survival() for the standard entry point. ## End(Not run)## Not run: # grf.subg.eval() is called internally to evaluate GRF subgroup quality. # See grf.subg.harm.survival() for the standard entry point. ## End(Not run)
Identifies treatment effect subgroups for binary, continuous, and count outcomes using Generalized Random Forests (GRF) for candidate factor screening, followed by exhaustive subgroup enumeration with GLM-based effect estimation.
grf.subg.harm.glm( data, confounders.name, outcome.name, treat.name = "treat", id.name = "id", outcome_type = c("binary", "continuous", "count"), effect_measure = NULL, offset.name = NULL, overdispersion = c("none", "quasi", "negbin"), grf_count_transform = c("log", "identity"), n.min = 60L, dmin.grf = 0, frac.tau = 0.5, maxdepth = 2L, RCT = TRUE, sg.criterion = c("mDiff", "Nsg"), conf.level = 0.95, seedit = 8316951L, return_selected_cuts_only = FALSE, adverse_outcome = FALSE, tune_grf = FALSE, grf_selection = c("tree", "frontier"), frontier_rule = c("effMaxSG", "eff", "maxSG", "minSG", "effMinSG"), effect_neighborhood = 0.1, selection_rule = c("neighborhood", "pareto", "both"), details = FALSE, verbose = FALSE )grf.subg.harm.glm( data, confounders.name, outcome.name, treat.name = "treat", id.name = "id", outcome_type = c("binary", "continuous", "count"), effect_measure = NULL, offset.name = NULL, overdispersion = c("none", "quasi", "negbin"), grf_count_transform = c("log", "identity"), n.min = 60L, dmin.grf = 0, frac.tau = 0.5, maxdepth = 2L, RCT = TRUE, sg.criterion = c("mDiff", "Nsg"), conf.level = 0.95, seedit = 8316951L, return_selected_cuts_only = FALSE, adverse_outcome = FALSE, tune_grf = FALSE, grf_selection = c("tree", "frontier"), frontier_rule = c("effMaxSG", "eff", "maxSG", "minSG", "effMinSG"), effect_neighborhood = 0.1, selection_rule = c("neighborhood", "pareto", "both"), details = FALSE, verbose = FALSE )
data |
Data frame containing the analysis data. |
confounders.name |
Character vector of potential effect modifier (confounder) names. These are the candidate split variables for GRF and the subgroup enumeration. |
outcome.name |
Character. Name of the outcome column. |
treat.name |
Character. Name of the binary treatment indicator (0/1).
Default: |
id.name |
Character. Name of the subject ID column.
Default: |
outcome_type |
Character. Type of outcome. One of |
effect_measure |
Character. Effect measure to estimate. One of
|
offset.name |
Character or |
overdispersion |
Character. Overdispersion correction for count models.
One of |
grf_count_transform |
Character. Transformation applied to the count
outcome before passing it to |
n.min |
Integer. Minimum subgroup sample size for a valid split.
Default: |
dmin.grf |
Numeric. Minimum absolute CATE magnitude (on the
doubly robust score scale) required for the identified subgroup to
be declared valid. The score equals |
frac.tau |
Numeric. Fraction of the sample used for the GRF horizon
(time-horizon analogue for non-survival outcomes). Default: |
maxdepth |
Integer. Maximum depth for GRF policy trees. Default: |
RCT |
Logical. Whether data come from a randomised trial. When
|
sg.criterion |
Character. Subgroup selection criterion. One of
|
conf.level |
Numeric. Confidence level for subgroup effect estimates.
Default: |
seedit |
Integer. Random seed for GRF. Default: |
return_selected_cuts_only |
Logical. If |
adverse_outcome |
Logical. If |
tune_grf |
Logical. If |
grf_selection |
Character, one of |
frontier_rule |
Character, one of |
effect_neighborhood |
Numeric in (0, 1); relative neighborhood for the
|
selection_rule |
Character, one of |
details |
Logical. Print GRF diagnostic information. Default: |
verbose |
Logical. Print progress messages. Default: |
For count / rate outcomes, the GRF screening step applies a
variance-stabilising transformation so that
grf::causal_forest() can be applied without dedicated
count-specific infrastructure. All subgroup effect estimates are
computed from correctly specified Poisson, quasi-Poisson, or
negative-binomial GLMs via create_glm_row.
grf::causal_forest() is designed for continuous outcomes. For count
data, this function transforms before passing it to GRF according
to the grf_count_transform argument:
"log" (default)Applies Y_star = log(Y + 0.5).
The +0.5 shift avoids log(0) for zero-count observations;
the log scale approximates variance stabilisation for Poisson data and
is recommended when any counts are zero or small.
"identity"Passes raw counts Y_star = Y directly.
Appropriate when counts are large, rarely zero, and the raw scale is
near-continuous (e.g., daily hospitalisation volumes in a large centre).
In both cases the transformation applies only to GRF factor
screening and policy-tree cut generation – it determines which
variables and which cut-points are candidate subgroup definitions.
The actual effect estimates in each subgroup are always computed from a
correctly specified Poisson (or quasi-Poisson / negative-binomial) GLM via
create_glm_row, regardless of grf_count_transform.
Alternatively, setting use_grf = FALSE in the parent
forestsearch call routes to LASSO-based factor selection,
which supports family = "poisson" natively via glmnet.
When offset.name is supplied, enters
every GLM fit in the subgroup enumeration loop, so that subgroup effects
are on the incidence-rate-ratio scale adjusted for differential follow-up.
A list of class "grf_glm_result" containing:
Character vector of cut expressions
defining the identified subgroup (e.g., c("v1=1", "v3=1")),
length equal to the depth of the selected policy tree.
NULL if no subgroup was found. Not a per-subject
membership indicator; see the Field naming collision
section below.
The input data frame with a treat.recommend column
added (0 = in harm/questionable subgroup, 1 =
complement).
Named list of GRF policy-tree split points.
Named numeric vector of GRF variable importances.
Character: name of the outcome column.
Character: name of the treatment column.
Character: name of the subject-identifier column.
Character vector of covariate column names.
Character: the effect measure used.
Character: the outcome type.
Character: the overdispersion correction applied.
Character or NULL: the offset variable name.
Character: the transform argument as matched
("log" or "identity").
Character: transformation actually applied to Y
for GRF screening ("log(Y + 0.5)", "identity", or
"none").
The field name sg.harm.id has different semantics on
this object versus on the grp.consistency list returned by
subgroup.consistency (and nested inside the
forestsearch result):
| Object | sg.harm.id contains |
Length / type |
grf.subg.harm.glm() result (this function) |
character vector of cut expressions | character, length = depth of selected tree |
subgroup.consistency() result |
per-subject 0/1 membership indicator | integer, length nrow(df)
|
Practical consequence. On this object, pasting
paste(obj$sg.harm.id, collapse = " & ") correctly renders
the identified subgroup. On a grp.consistency list the same
expression silently concatenates a long 0/1 indicator and produces
output like "0 & 0 & 1 & 0 & ...". When writing code that
must handle both object types, dispatch on class first, or use the
top-level sg.harm field that the forestsearch
result exposes.
This naming collision is a documented CRAN-stable API for v0.1.x and v0.2.x; it is expected to be resolved via a deprecation cycle in a future minor release.
grf.subg.harm.survival for time-to-event outcomes,
create_glm_row for per-subgroup GLM estimation,
glm_effect_profile for continuous biomarker profiles.
## Not run: # Simulate count data with heterogeneous treatment effect set.seed(123) n <- 600 z1 <- rbinom(n, 1, 0.4) # binary subgroup factor df_count <- data.frame( id = seq_len(n), treat = rbinom(n, 1, 0.5), v1 = as.factor(z1), v2 = as.factor(rbinom(n, 1, 0.5)), person_time = runif(n, 0.5, 2.0), age = rnorm(n, 60, 10) ) # Generate outcome: higher event rate for treated in z1 = 1 subgroup log_rate <- -1.5 + 0.8 * df_count$treat * (df_count$v1 == 1) - 0.3 * df_count$treat * (df_count$v1 == 0) + log(df_count$person_time) df_count$events <- rpois(n, lambda = exp(log_rate)) # Run GRF subgroup identification for count outcome (log transform, default) grf_glm <- grf.subg.harm.glm( data = df_count, confounders.name = c("v1", "v2", "age"), outcome.name = "events", treat.name = "treat", id.name = "id", outcome_type = "count", effect_measure = "log_IRR", offset.name = "person_time", overdispersion = "quasi", grf_count_transform = "log", # default: log(Y + 0.5) n.min = 60, verbose = TRUE ) print(grf_glm$sg.harm.id) # Alternative: identity transform (large counts, no zeros) grf_glm_id <- grf.subg.harm.glm( data = df_count, confounders.name = c("v1", "v2", "age"), outcome.name = "events", treat.name = "treat", id.name = "id", outcome_type = "count", effect_measure = "log_IRR", offset.name = "person_time", overdispersion = "none", grf_count_transform = "identity", # raw counts passed to GRF n.min = 60 ) ## End(Not run)## Not run: # Simulate count data with heterogeneous treatment effect set.seed(123) n <- 600 z1 <- rbinom(n, 1, 0.4) # binary subgroup factor df_count <- data.frame( id = seq_len(n), treat = rbinom(n, 1, 0.5), v1 = as.factor(z1), v2 = as.factor(rbinom(n, 1, 0.5)), person_time = runif(n, 0.5, 2.0), age = rnorm(n, 60, 10) ) # Generate outcome: higher event rate for treated in z1 = 1 subgroup log_rate <- -1.5 + 0.8 * df_count$treat * (df_count$v1 == 1) - 0.3 * df_count$treat * (df_count$v1 == 0) + log(df_count$person_time) df_count$events <- rpois(n, lambda = exp(log_rate)) # Run GRF subgroup identification for count outcome (log transform, default) grf_glm <- grf.subg.harm.glm( data = df_count, confounders.name = c("v1", "v2", "age"), outcome.name = "events", treat.name = "treat", id.name = "id", outcome_type = "count", effect_measure = "log_IRR", offset.name = "person_time", overdispersion = "quasi", grf_count_transform = "log", # default: log(Y + 0.5) n.min = 60, verbose = TRUE ) print(grf_glm$sg.harm.id) # Alternative: identity transform (large counts, no zeros) grf_glm_id <- grf.subg.harm.glm( data = df_count, confounders.name = c("v1", "v2", "age"), outcome.name = "events", treat.name = "treat", id.name = "id", outcome_type = "count", effect_measure = "log_IRR", offset.name = "person_time", overdispersion = "none", grf_count_transform = "identity", # raw counts passed to GRF n.min = 60 ) ## End(Not run)
Identifies subgroups with differential treatment effect using generalized random forests (GRF) and policy trees. This function uses causal survival forests to identify heterogeneous treatment effects and policy trees to create interpretable subgroup definitions.
grf.subg.harm.survival( data, confounders.name, outcome.name, event.name, id.name, treat.name, frac.tau = 1, n.min = 60, dmin.grf = 0, RCT = TRUE, details = FALSE, sg.criterion = "mDiff", maxdepth = 2, seedit = 8316951, return_selected_cuts_only = FALSE, tune_grf = FALSE, grf_selection = c("tree", "frontier"), frontier_rule = c("effMaxSG", "eff", "maxSG", "minSG", "effMinSG"), effect_neighborhood = 0.1, selection_rule = c("neighborhood", "pareto", "both") )grf.subg.harm.survival( data, confounders.name, outcome.name, event.name, id.name, treat.name, frac.tau = 1, n.min = 60, dmin.grf = 0, RCT = TRUE, details = FALSE, sg.criterion = "mDiff", maxdepth = 2, seedit = 8316951, return_selected_cuts_only = FALSE, tune_grf = FALSE, grf_selection = c("tree", "frontier"), frontier_rule = c("effMaxSG", "eff", "maxSG", "minSG", "effMinSG"), effect_neighborhood = 0.1, selection_rule = c("neighborhood", "pareto", "both") )
data |
Data frame containing the analysis data. |
confounders.name |
Character vector of confounder variable names. |
outcome.name |
Character. Name of outcome variable (e.g., time-to-event). |
event.name |
Character. Name of event indicator variable (0/1). |
id.name |
Character. Name of ID variable. |
treat.name |
Character. Name of treatment group variable (0/1). |
frac.tau |
Numeric. Fraction of tau for GRF horizon (default: 1.0). |
n.min |
Integer. Minimum subgroup size (default: 60). |
dmin.grf |
Numeric. Minimum difference in subgroup mean (default: 0.0). |
RCT |
Logical. Is the data from a randomized controlled trial? (default: TRUE) |
details |
Logical. Print details during execution (default: FALSE). |
sg.criterion |
Character. Subgroup selection criterion ("mDiff" or "Nsg"). |
maxdepth |
Integer. Maximum tree depth (1, 2, or 3; default: 2). |
seedit |
Integer. Random seed (default: 8316951). |
return_selected_cuts_only |
Logical. If TRUE, returns only cuts from the tree
depth that identified the selected subgroup meeting |
tune_grf |
Logical. If |
grf_selection |
Character, one of |
frontier_rule |
Character, one of |
effect_neighborhood |
Numeric in (0, 1); relative neighborhood for the
|
selection_rule |
Character, one of |
The return_selected_cuts_only parameter controls which cuts are returned:
Returns all cuts from all fitted trees (depths 1 to
maxdepth). This provides the full set of candidate splits for downstream
exploration and is the original behavior for backward compatibility.
Returns only cuts from the tree at the depth that identified
the "winning" subgroup meeting the dmin.grf criterion. This is useful
when you want a focused set of cuts associated with the selected subgroup,
reducing noise from non-selected trees.
When return_selected_cuts_only = TRUE and no subgroup meets the criteria,
tree.cuts will be empty (character(0)).
A list with GRF results, including:
data |
Original data with added treatment recommendation flags |
grf.gsub |
Selected subgroup information |
sg.harm.id |
Character vector of cut expressions
defining the identified subgroup (length = depth of the selected
policy tree), or |
tree.cuts |
Cut expressions - either all cuts from all trees (if
|
tree.names |
Unique variable names used in cuts |
tree |
Selected policy tree object |
tau.rmst |
Time horizon used for RMST |
harm.any |
All subgroups with positive treatment effect difference |
selected_depth |
Depth of the tree that identified the subgroup (when found) |
return_selected_cuts_only |
Logical indicating which cut extraction mode was used |
Additional tree-specific cuts and objects (tree1, tree2, tree3) based on maxdepth
The field name sg.harm.id has different semantics on
this object versus on the grp.consistency list returned by
subgroup.consistency (and nested inside the
forestsearch result):
| Object | sg.harm.id contains |
Length / type |
grf.subg.harm.survival() result (this function) |
character vector of cut expressions | character, length = depth of selected tree |
subgroup.consistency() result |
per-subject 0/1 membership indicator | integer, length nrow(data)
|
Practical consequence. On this object, pasting
paste(obj$sg.harm.id, collapse = " & ") correctly renders
the identified subgroup. On a grp.consistency list the same
expression silently concatenates a long 0/1 indicator and produces
output like "0 & 0 & 1 & 0 & ...". When writing code that
must handle both object types, dispatch on class first, or use the
top-level sg.harm field that the forestsearch
result exposes.
This naming collision is a documented CRAN-stable API for v0.1.x and v0.2.x; it is expected to be resolved via a deprecation cycle in a future minor release.
## Not run: # Return all cuts (default behavior) result_all <- grf.subg.harm.survival( data = trial_data, confounders.name = c("age", "biomarker", "region"), outcome.name = "tte", event.name = "event", id.name = "id", treat.name = "treat", dmin.grf = 0.1, maxdepth = 2 ) result_all$tree.cuts # Returns cuts from both depth 1 and depth 2 trees # Return only cuts from the selected tree result_selected <- grf.subg.harm.survival( data = trial_data, confounders.name = c("age", "biomarker", "region"), outcome.name = "tte", event.name = "event", id.name = "id", treat.name = "treat", dmin.grf = 0.1, maxdepth = 2, return_selected_cuts_only = TRUE ) result_selected$tree.cuts # Returns cuts only from the depth that identified the winning subgroup ## End(Not run)## Not run: # Return all cuts (default behavior) result_all <- grf.subg.harm.survival( data = trial_data, confounders.name = c("age", "biomarker", "region"), outcome.name = "tte", event.name = "event", id.name = "id", treat.name = "treat", dmin.grf = 0.1, maxdepth = 2 ) result_all$tree.cuts # Returns cuts from both depth 1 and depth 2 trees # Return only cuts from the selected tree result_selected <- grf.subg.harm.survival( data = trial_data, confounders.name = c("age", "biomarker", "region"), outcome.name = "tte", event.name = "event", id.name = "id", treat.name = "treat", dmin.grf = 0.1, maxdepth = 2, return_selected_cuts_only = TRUE ) result_selected$tree.cuts # Returns cuts only from the depth that identified the winning subgroup ## End(Not run)
Implements Algorithm 2 of Guo and He (2021), Section 2.5: a v-fold
cross-validated choice of the shrinkage tuning parameter r used by
guohe_algorithm3(). For each candidate r and each fold, the
bias-reduced estimator is computed on the training folds and compared, on
the held-out fold, against every candidate subgroup's effect estimate with
its sampling variance subtracted; the r minimising the resulting CV
objective is selected.
guohe_adaptive_r( data, outcome = c("survival", "binary", "continuous"), treatment, candidates, time = NULL, event = NULL, y = NULL, orient = -1, r_grid = c(0.03, 0.1, 0.2, 0.3, 0.4, 0.45), v = 5L, B = 200L, level = 0.05, seed = NULL, min_events = 5L, refit = TRUE, adjust_covariates = NULL, fast = NULL, parallel = FALSE )guohe_adaptive_r( data, outcome = c("survival", "binary", "continuous"), treatment, candidates, time = NULL, event = NULL, y = NULL, orient = -1, r_grid = c(0.03, 0.1, 0.2, 0.3, 0.4, 0.45), v = 5L, B = 200L, level = 0.05, seed = NULL, min_events = 5L, refit = TRUE, adjust_covariates = NULL, fast = NULL, parallel = FALSE )
data |
A data frame. |
outcome |
One of |
treatment |
Name of the 0/1 treatment column. |
candidates |
Character vector of names of 0/1 membership columns forming the enumerated family. |
time, event
|
Survival only: names of the time and event columns. |
y |
Binary or continuous only: name of the outcome column. |
orient |
Either |
r_grid |
Numeric vector of candidate values in (0, 0.5). |
v |
Number of cross-validation folds. |
B |
Number of bootstrap resamples. |
level |
One-sided level; the two-sided interval has coverage |
seed |
Optional integer for reproducible resampling. |
min_events |
Minimum events (survival) or minimum per-arm count for a candidate to be estimable. |
refit |
Logical; if |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Optional character vector of covariate names for the
within-subgroup model, matching the argument of the same name in
NOTE: |
fast |
Logical or |
parallel |
Logical; if |
This is an independent implementation of the published method, validated by reproducing the simulation study of Section 5 of Guo and He (2021).
An object of class "guohe_ar": the selected r (r_hat), the CV
objective over the grid (objective), the per-candidate objective at the
selected r (per_candidate), bookkeeping fields (r_grid, v, B,
n, n_candidates, fast, outcome, orient, level,
adjust_covariates), and, when refit = TRUE, the refitted full-data
guohe_algorithm3() result (fit).
Guo, X. and He, X. (2021). Inference on Selected Subgroups in Clinical Trials. Journal of the American Statistical Association, 116(535), 1498–1506. doi:10.1080/01621459.2020.1740096
set.seed(1) n <- 160 df <- data.frame( treat = rbinom(n, 1, 0.5), y = rbinom(n, 1, 0.3), g1 = rbinom(n, 1, 0.5), g2 = rbinom(n, 1, 0.5), g3 = rbinom(n, 1, 0.5) ) cv <- guohe_adaptive_r( df, outcome = "binary", treatment = "treat", candidates = c("g1", "g2", "g3"), y = "y", orient = +1, r_grid = c(0.05, 0.25), v = 2, B = 25, seed = 2 ) cv$r_hatset.seed(1) n <- 160 df <- data.frame( treat = rbinom(n, 1, 0.5), y = rbinom(n, 1, 0.3), g1 = rbinom(n, 1, 0.5), g2 = rbinom(n, 1, 0.5), g3 = rbinom(n, 1, 0.5) ) cv <- guohe_adaptive_r( df, outcome = "binary", treatment = "treat", candidates = c("g1", "g2", "g3"), y = "y", orient = +1, r_grid = c(0.05, 0.25), v = 2, B = 25, seed = 2 ) cv$r_hat
Finite realization of Algorithm 3 of Guo and He (2021): the candidate family is supplied as materialized 0/1 membership columns, the most extreme oriented effect is selected, and its selection optimism is removed by a pair (case) bootstrap with deterministic shrinkage offsets computed against the global maximum. Survival, binary, and continuous outcomes are supported.
guohe_algorithm3( data, outcome = c("survival", "binary", "continuous"), treatment, candidates, time = NULL, event = NULL, y = NULL, orient = -1, B = 2000L, r = 0.03, level = 0.05, seed = NULL, min_events = 5L, diagnostics = TRUE, parallel = FALSE, adjust_covariates = NULL, fast = NULL )guohe_algorithm3( data, outcome = c("survival", "binary", "continuous"), treatment, candidates, time = NULL, event = NULL, y = NULL, orient = -1, B = 2000L, r = 0.03, level = 0.05, seed = NULL, min_events = 5L, diagnostics = TRUE, parallel = FALSE, adjust_covariates = NULL, fast = NULL )
data |
A data frame. |
outcome |
One of |
treatment |
Name of the 0/1 treatment column. |
candidates |
Character vector of names of 0/1 membership columns forming the enumerated family. |
time, event
|
Survival only: names of the time and event columns. |
y |
Binary or continuous only: name of the outcome column. |
orient |
Either |
B |
Number of bootstrap resamples. |
r |
Shrinkage tuning parameter, strictly between 0 and 0.5. Guo and He's Algorithm 2 gives a cross-validated choice; a fixed small value is used here. |
level |
One-sided level; the two-sided interval has coverage |
seed |
Optional integer for reproducible resampling. |
min_events |
Minimum events (survival) or minimum per-arm count for a candidate to be estimable. |
diagnostics |
Logical; if |
parallel |
Logical; if |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Optional character vector of covariate names for the
within-subgroup model, matching the argument of the same name in
NOTE: |
fast |
Logical or |
Membership is computed once on the full data and carried through the bootstrap; cut points are NOT re-derived per resample. This is required by the method (the index set is held fixed) and is what makes the comparison against multiplier resampling like-for-like; regenerating the family per resample is the full-bootstrap object instead.
The oriented score is orient * b, with b the within-subgroup treatment
coefficient on the natural scale. orient = -1 treats a protective ratio
(HR or OR below 1) as the effect of interest, matching Guo and He's MONET1
convention; orient = +1 treats a harmful ratio as the effect of interest,
which is the setting for a forest-search harm subgroup.
This is an independent implementation of the published method, validated by reproducing the simulation study of Section 5 of Guo and He (2021).
An object of class "guohe_a3": a list with the selected candidate
(selected), the naive and de-biased effects on the reporting scale
(naive, debiased), the one-sided bound of Guo and He's Algorithm 3 (bound_one_sided,
with its conservative side in bound_side), the two-sided
quantile-inversion interval (ci_two_sided), the heuristic Wald interval
(ci_wald), and bookkeeping fields (n, B, B_requested, r,
level, n_candidates, n_estimable, fast, lean_cox,
adjust_covariates, and, when diagnostics = TRUE, the per-candidate
resample drop_rate).
Guo, X. and He, X. (2021). Inference on Selected Subgroups in Clinical Trials. Journal of the American Statistical Association, 116(535), 1498–1506. doi:10.1080/01621459.2020.1740096
set.seed(1) n <- 160 df <- data.frame( treat = rbinom(n, 1, 0.5), y = rbinom(n, 1, 0.3), g1 = rbinom(n, 1, 0.5), g2 = rbinom(n, 1, 0.5), g3 = rbinom(n, 1, 0.5) ) fit <- guohe_algorithm3( df, outcome = "binary", treatment = "treat", candidates = c("g1", "g2", "g3"), y = "y", orient = +1, B = 50, seed = 2 ) fitset.seed(1) n <- 160 df <- data.frame( treat = rbinom(n, 1, 0.5), y = rbinom(n, 1, 0.3), g1 = rbinom(n, 1, 0.5), g2 = rbinom(n, 1, 0.5), g3 = rbinom(n, 1, 0.5) ) fit <- guohe_algorithm3( df, outcome = "binary", treatment = "treat", candidates = c("g1", "g2", "g3"), y = "y", orient = +1, B = 50, seed = 2 ) fit
Formats a hazard ratio and confidence interval for display.
hrCI_format(hrest)hrCI_format(hrest)
hrest |
Numeric vector with HR, lower, and upper confidence limits. |
Character string formatted as \"HR (lower, upper)\".
hrCI_format(c(1.45, 0.98, 2.14))hrCI_format(c(1.45, 0.98, 2.14))
Build an aft_dgm_flex with synthetic regions and paper-style effect structure.
inject_mrct_structure( seed_data, outcome_var, event_var, treatment_var, continuous_vars, factor_vars, x_pred, spline_spec, region = list(), x3 = NULL, region_treat_loghr = NULL, n_super = 5000L, expand = c("copula", "bootstrap"), continuous_vars_cens = NULL, factor_vars_cens = NULL, cens_type = "weibull", seed = 8316951L, verbose = FALSE )inject_mrct_structure( seed_data, outcome_var, event_var, treatment_var, continuous_vars, factor_vars, x_pred, spline_spec, region = list(), x3 = NULL, region_treat_loghr = NULL, n_super = 5000L, expand = c("copula", "bootstrap"), continuous_vars_cens = NULL, factor_vars_cens = NULL, cens_type = "weibull", seed = 8316951L, verbose = FALSE )
seed_data |
seed trial data.frame (one row per patient) |
outcome_var, event_var, treatment_var
|
column names in seed_data |
continuous_vars, factor_vars
|
covariates entering the AFT model (factor_vars must be 0/1 numeric or 2-level factors for clean expansion) |
x_pred |
region-associated effect modifier (X2), a continuous_var |
spline_spec |
list(knot, zeta, log_hrs) on the NATURAL scale of x_pred:
true log-HR is piecewise linear through (0, |
region |
list(name = "region", prevalence = 0.2, or_pred = 10,
x1_vars = character(0), or_x1 = 1, loghr = 0, scale = "rank") – loghr is
the prognostic log-HR of Region (no TE modification; 0 for the paper's
DGM; -log(5) reproduces the Feb-2026 deck's strongly prognostic region);
scale = "rank" (ECDF) or "minmax" (the paper's |
x3 |
NULL, or list(vars = c(...), cuts = list(...), loghr = ...) for a balanced effect modifier: cuts as in generate_aft_dgm_flex (use fixed values), loghr = log-HR change (treatment x subgroup). |
region_treat_loghr |
NULL, or a log-HR for a direct treat x Region interaction (the residual Region-U-TE pathway). Mutually exclusive with x3 (the package supports one interaction term). |
n_super |
size of the expanded super-population |
expand |
"copula" or "bootstrap" |
continuous_vars_cens, factor_vars_cens, cens_type
|
passed through |
seed |
RNG seed |
verbose |
logical |
aft_dgm_flex with df_super = expanded population (+ z_<region>), and
a $mrct element documenting the injected structure.
Produces a templated text summary of the estimation properties table, automatically populating numerical results from the simulation output. Useful for reproducible vignettes where interpretation paragraphs should update when simulations are re-run.
interpret_estimation_table( results, dgm, analysis_method = "FSlg", n_sims = NULL, n_boots = 300, digits = 2, scenario = NULL, cat = TRUE, subgroup_notation = c("harm", "benefit"), trim_threshold = 1000, trim_fraction = 0.01 )interpret_estimation_table( results, dgm, analysis_method = "FSlg", n_sims = NULL, n_boots = 300, digits = 2, scenario = NULL, cat = TRUE, subgroup_notation = c("harm", "benefit"), trim_threshold = 1000, trim_fraction = 0.01 )
results |
Data frame of simulation results (same as for
|
dgm |
DGM object with true parameter values. |
analysis_method |
Character. Which analysis method to summarise.
Default: |
n_sims |
Integer. Total number of simulations (for detection rate).
If |
n_boots |
Integer. Number of bootstraps (for narrative). Default: 300. |
digits |
Integer. Decimal places for reported values. Default: 2. |
scenario |
Character. One of
If |
cat |
Logical. If |
subgroup_notation |
Character. |
trim_threshold |
Numeric or |
trim_fraction |
Numeric in |
Invisibly returns the interpretation as a character string.
build_estimation_table,
format_oc_results, get_dgm_hr
## Not run: # In a vignette chunk with results = "asis": interpret_estimation_table(results_null, dgm_null, scenario = "null") # Capture for further processing: txt <- interpret_estimation_table(results_alt, dgm_alt, cat = FALSE) ## End(Not run)## Not run: # In a vignette chunk with results = "asis": interpret_estimation_table(results_null, dgm_null, scenario = "null") # Capture for further processing: txt <- interpret_estimation_table(results_alt, dgm_alt, cat = FALSE) ## End(Not run)
Generates a human-readable diagnostic of how ForestSearch will
interpret the user's parameter combination. Prints via
message() and returns an invisible list of diagnostics.
interpret_search_config( outcome_type, effect_measure, adverse_outcome, effect_threshold, consistency_threshold, use_lasso, use_grf, outcome.name, event.name, treat.name, offset.name = NULL, subgroup_method = "consistency", quiet = FALSE )interpret_search_config( outcome_type, effect_measure, adverse_outcome, effect_threshold, consistency_threshold, use_lasso, use_grf, outcome.name, event.name, treat.name, offset.name = NULL, subgroup_method = "consistency", quiet = FALSE )
outcome_type |
Character. One of |
effect_measure |
Character. |
adverse_outcome |
Logical. As resolved by forestsearch(). |
effect_threshold |
Numeric. Resolved screening threshold (on log scale for ratio measures, identity for others). |
consistency_threshold |
Numeric. Resolved consistency threshold. |
use_lasso |
Logical. |
use_grf |
Logical. Whether GRF candidate-cut generation is on;
see |
outcome.name |
Character. Name of outcome column. |
event.name |
Character. Name of event column. |
treat.name |
Character. Name of treatment column. |
offset.name |
Character or NULL. |
subgroup_method |
Character, the resolved |
quiet |
Logical. If TRUE, suppress output. |
Invisible list with diagnostic fields.
Performs LASSO variable selection using Cox regression.
lasso_selection( df, confounders.name, outcome.name, event.name, seedit = 8316951, outcome_type = "survival", offset.name = NULL )lasso_selection( df, confounders.name, outcome.name, event.name, seedit = 8316951, outcome_type = "survival", offset.name = NULL )
df |
Data frame. |
confounders.name |
Character vector of confounder names. |
outcome.name |
Character. Name of outcome variable. |
event.name |
Character. Name of event indicator variable. |
seedit |
Integer. Random seed. |
outcome_type |
Character. One of |
offset.name |
Character or |
List with selected, omitted variables, coefficients, lambda, and fits.
library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") lasso_selection(df, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", event.name = "status")library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") lasso_selection(df, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", event.name = "status")
Scans output_dir for files matching the given prefix
pattern and returns the path to the most recently modified one.
Date-stamped filenames (e.g., results_20260402.rds) sort
naturally, but this function uses filesystem modification time
for robustness.
latest_rds(prefix, output_dir = "_data", verbose = TRUE)latest_rds(prefix, output_dir = "_data", verbose = TRUE)
prefix |
Character. Filename prefix to match
(e.g., |
output_dir |
Character. Directory to scan.
Default: |
verbose |
Logical. Print the matched file. Default: TRUE. |
Full path to the most recent matching file, or NULL if no match is found.
## Not run: path <- latest_rds("selection_survival_frontier") res <- readRDS(path) path <- latest_rds("calibration_binary_leff_grid") ## End(Not run)## Not run: path <- latest_rds("selection_survival_frontier") res <- readRDS(path) path <- latest_rds("calibration_binary_leff_grid") ## End(Not run)
Reads a calibration bundle saved by save_leff_calibration().
Prints a summary and returns the full bundle.
load_leff_calibration(outcome_type, output_dir = NULL, verbose = TRUE)load_leff_calibration(outcome_type, output_dir = NULL, verbose = TRUE)
outcome_type |
Character. One of "binary", "survival", "count", "continuous". |
output_dir |
Character. Directory containing .rds files.
Default: |
verbose |
Logical. Print summary on load. Default: TRUE. |
A list with components C, alpha, n_min, outcome_type, cal_data, dgm_description, timestamp, etc.
## Not run: cal <- load_leff_calibration("binary") C_prior <- cal$C alpha_prior <- cal$alpha ## End(Not run)## Not run: cal <- load_leff_calibration("binary") C_prior <- cal$C alpha_prior <- cal$alpha ## End(Not run)
Returns a closure function(data_slice) that estimates the
within-subgroup treatment effect for the requested outcome type and
effect measure. The closure is called repeatedly inside the
splitting-consistency loop and the bootstrap worker, so it must be
lightweight and handle small-sample edge cases gracefully.
make_effect_estimator( outcome_type, treat.name, outcome.name, event.name = NULL, offset.name = NULL, effect_measure = NULL, adverse_outcome = TRUE, robust_se = TRUE, adjust_covariates = NULL, ps_adjust_method = c("none", "iptw", "dr_gcomp"), ... )make_effect_estimator( outcome_type, treat.name, outcome.name, event.name = NULL, offset.name = NULL, effect_measure = NULL, adverse_outcome = TRUE, robust_se = TRUE, adjust_covariates = NULL, ps_adjust_method = c("none", "iptw", "dr_gcomp"), ... )
outcome_type |
Character. One of |
treat.name |
Character. Name of the binary treatment column (0/1). |
outcome.name |
Character. Name of the outcome column. |
event.name |
Character or |
offset.name |
Character or |
effect_measure |
Character or |
adverse_outcome |
Logical. If |
robust_se |
Logical. Use sandwich robust SE when available.
Default |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
ps_adjust_method |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character. Propensity score adjustment method.
One of |
... |
Additional arguments (reserved for future extensions). |
A closure function(data_slice) returning a list with
components: estimate, se, converged, n0,
n1, measure, method_used.
## Not run: # Binary outcome with odds ratio fn <- make_effect_estimator("binary", "treat", "event", effect_measure = "OR") result <- fn(my_data) exp(result$estimate) # odds ratio # Poisson rate with IPTW adjustment fn_iptw <- make_effect_estimator("binary", "treat", "event", offset.name = "time", effect_measure = "IRR", ps_adjust_method = "iptw") ## End(Not run)## Not run: # Binary outcome with odds ratio fn <- make_effect_estimator("binary", "treat", "event", effect_measure = "OR") result <- fn(my_data) exp(result$estimate) # odds ratio # Poisson rate with IPTW adjustment fn_iptw <- make_effect_estimator("binary", "treat", "event", offset.name = "time", effect_measure = "IRR", ps_adjust_method = "iptw") ## End(Not run)
Build the logistic region model and draw Region on a population.
make_region_model( pop, x_pred = NULL, or_pred = 1, x1_vars = character(0), or_x1 = 1, prevalence = 0.2, scale_ref = NULL, scale = c("rank", "minmax"), band = NULL )make_region_model( pop, x_pred = NULL, or_pred = 1, x1_vars = character(0), or_x1 = 1, prevalence = 0.2, scale_ref = NULL, scale = c("rank", "minmax"), band = NULL )
pop |
data.frame holding x_pred and x1_vars (natural scale) |
x_pred |
name of the region-associated effect modifier (X2); NULL for no X2 term (regional imbalance only through x1_vars) |
or_pred |
odds ratio for s(x_pred) -> Region (OR = 1: none) |
x1_vars |
names of imbalanced-only covariates (X1); character(0) for none |
or_x1 |
odds ratios for the X1 terms (recycled) |
prevalence |
target marginal P(Region = 1) |
scale_ref |
data.frame used to define the scaling (defaults to pop) |
scale |
"rank" (ECDF, default) or "minmax" (the paper's scaling) |
band |
NULL (default, no band term), or list(var, cut, or): adds
|
list(alpha0, coefs, band, prob = function(df), draw = function(df, seed))
Renders the selected subgroup's MR de-biased estimate (forestsearch() with
mr_inference = TRUE) in the same gt layout as the bias-corrected table
produced by summarize_bootstrap_results(). The descriptive columns
(sample size, medians/RMST or rates) and the naive effect do not depend on
which de-biasing method was used, so they are inherited from a full-bootstrap
(FB) result used as a template; the bias-corrected column is replaced with
the MR values from fs$mr_inference – the de-biased estimate for the
selected subgroup and, when MR was run with include_complement = TRUE, for
the complement as well (otherwise the complement cell is marked, since MR
de-biases the selected subgroup only).
mr_estimates_table(fs, boot_results, est.scale = "hr", digits = 2)mr_estimates_table(fs, boot_results, est.scale = "hr", digits = 2)
fs |
A |
boot_results |
A |
est.scale |
Character, |
digits |
Integer display precision (default |
The selected-subgroup row is matched by its bias-corrected string
(boot_results$SG_CIs$H_bc) rather than by position, so the substitution is
robust to row ordering. The MR de-biased confidence interval is a
leading-order approximation of the full-bootstrap infinitesimal-jackknife
interval – it comes from the IJ variance computed on the multiplier draws
under ci_method = "ij" or "field" (the forest-search path defaults to
"field", which adds the field bounds; the DINA and GRF paths default to
"ij"), and from the subgroup robust SE only under ci_method = "wald".
Either way the bias-corrected cell may differ
slightly from the full bootstrap even when the naive effect matches exactly.
A gt table.
forestsearch() for the mr_inference switch and the vocabulary
section; fs_mr_inference() for the MR computation itself;
forestsearch_bootstrap_dofuture() for the full bootstrap that supplies
the template; fs_fdr_report() for harm-confirmation threshold sweeps;
summarize_bootstrap_results(), format_bootstrap_table()
Simulates multi-regional clinical trials and evaluates ForestSearch subgroup identification. Splits data by region into training and testing populations, identifies subgroups using ForestSearch on training data, and evaluates performance on the testing region.
mrct_region_sims( dgm, n_sims, n_sample = NULL, region_var = "z_regA", sg_focus = "minSG", maxk = 1, hr.threshold = 0.9, hr.consistency = 0.8, pconsistency.threshold = 0.9, confounders.name = NULL, conf_force = NULL, fs_args = list(), sim_args = list(rand_ratio = 1, draw_treatment = TRUE), analysis_time = 60, cens_adjust = 0, parallel_args = list(plan = "multisession", workers = NULL, show_message = TRUE), details = FALSE, verbose_n_sims = 2L, seed = NULL )mrct_region_sims( dgm, n_sims, n_sample = NULL, region_var = "z_regA", sg_focus = "minSG", maxk = 1, hr.threshold = 0.9, hr.consistency = 0.8, pconsistency.threshold = 0.9, confounders.name = NULL, conf_force = NULL, fs_args = list(), sim_args = list(rand_ratio = 1, draw_treatment = TRUE), analysis_time = 60, cens_adjust = 0, parallel_args = list(plan = "multisession", workers = NULL, show_message = TRUE), details = FALSE, verbose_n_sims = 2L, seed = NULL )
dgm |
Data generating mechanism object from |
n_sims |
Integer. Number of simulations to run |
n_sample |
Integer. Sample size per simulation. If NULL (default), uses the entire super-population from dgm |
region_var |
Character. Name of the region indicator variable used to split data into training (region_var == 0) and testing (region_var == 1) populations. Default: "z_regA" |
sg_focus |
Character. Subgroup selection criterion passed to
|
maxk |
Integer. Maximum number of factors in subgroup combinations (1 or 2). Default: 1 |
hr.threshold |
Numeric. |
hr.consistency |
Numeric. |
pconsistency.threshold |
Numeric. |
confounders.name |
Character vector. Confounder variable names for ForestSearch. If NULL, automatically extracted from dgm |
conf_force |
Character vector. Forced cuts to consider in ForestSearch. Default: c("z_age <= 65", "z_bm <= 0", "z_bm <= 1", "z_bm <= 2", "z_bm <= 5") |
fs_args |
Named list. Additional arguments passed directly to
|
sim_args |
Named list. Additional arguments passed to
|
analysis_time |
Numeric. Time of analysis for administrative censoring. Default: 60 |
cens_adjust |
Numeric. Adjustment factor for censoring rate on log scale. Default: 0 |
parallel_args |
List. Parallel processing configuration with components:
|
details |
Logical. Print detailed progress information. Default: FALSE |
verbose_n_sims |
Integer. When |
seed |
Integer. Base random seed for reproducibility. Default: NULL |
For each simulation:
Sample from super-population using simulate_from_dgm
Split by region_var into training and testing populations
Estimate HRs in ITT, training, and testing populations
Run forestsearch on training population
Apply identified subgroup to testing population
Calculate subgroup-specific estimates
The region_var parameter is used ONLY for splitting data into training/testing
populations. It does not imply any prognostic effect. To include prognostic
confounder effects, specify them when creating the DGM using
create_dgm_for_mrct or generate_aft_dgm_flex.
A data.table with simulation results containing:
Simulation index
ITT sample size
ITT hazard ratio (stratified if strat variable present)
ITT hazard ratio stratified by region
Training (non-region A) sample size
Training population hazard ratio
Testing (region A) sample size
Testing population hazard ratio
Indicator: 1 if subgroup identified, 0 otherwise
Character description of identified subgroup
Subgroup sample size
Subgroup hazard ratio in testing population
Potential outcome hazard ratio in subgroup (testing)
Subgroup prevalence (proportion of testing population)
Subgroup sample size in training population
Subgroup hazard ratio in training population
Potential outcome hazard ratio in subgroup (training)
Subgroup HR when found, NA otherwise
forestsearch for subgroup identification algorithm
generate_aft_dgm_flex for DGM creation
simulate_from_dgm for data simulation
create_dgm_for_mrct for MRCT-specific DGM wrapper
summaryout_mrct for summarizing simulation results
## Not run: # Create DGM for alternative hypothesis dgm_alt <- create_dgm_for_mrct( df_case = df_case, model_type = "alt", log_hrs = log(c(3, 1.25, 0.50)), verbose = TRUE ) # Run simulations results <- mrct_region_sims( dgm = dgm_alt, n_sims = 100, region_var = "z_regA", sg_focus = "minSG", parallel_args = list(plan = "multisession", workers = 4), details = TRUE ) # Summarize results cat("Subgroup identification rate:", mean(results$any_found) * 100, "%\n") ## End(Not run)## Not run: # Create DGM for alternative hypothesis dgm_alt <- create_dgm_for_mrct( df_case = df_case, model_type = "alt", log_hrs = log(c(3, 1.25, 0.50)), verbose = TRUE ) # Run simulations results <- mrct_region_sims( dgm = dgm_alt, n_sims = 100, region_var = "z_regA", sg_focus = "minSG", parallel_args = list(plan = "multisession", workers = 4), details = TRUE ) # Summarize results cat("Subgroup identification rate:", mean(results$any_found) * 100, "%\n") ## End(Not run)
Truth by region on the super-population: prevalence, x_pred shift, causal AHR / CDE, and a large-sample Cox HR on UNCENSORED potential outcomes. Compare HR_cox_PO with the censored-trial HR from sim_region_metrics(): a gap between them isolates the prognostic-shift / follow-up-truncation mechanism (the Feb-2026 deck) from covariate shift (AHR itself differing by region).
mrct_truth_by_region(dgm, seed = 1L)mrct_truth_by_region(dgm, seed = 1L)
dgm |
An MRCT data-generating mechanism returned by
|
seed |
Integer seed passed to |
A data frame with one row per stratum ("ALL", "NON-REGION",
"REGION") and columns stratum, n, prev, x_pred_mean, AHR,
CDE and HR_cox_PO. Attribute "smd_x_pred" holds the standardised
mean difference of the effect modifier between region and non-region.
Returns count and percent for a vector relative to a denominator.
n_pcnt(x, denom)n_pcnt(x, denom)
x |
Vector of values. |
denom |
Denominator for percent calculation. |
Character string formatted as \"n (percent%)\".
n_pcnt(1:30, 100)n_pcnt(1:30, 100)
Renders the post-hoc Pareto frontier on (effect, N) – both maximized
– as a formatted gt table or returns it as a
data.table for programmatic use. Works uniformly across
survival (Cox PH) and GLM outcome types: the effect-column label and
scale handling are derived from the forestsearch object's
effect_measure.
pareto_frontier_table( fs, format = c("gt", "data.table"), digits = 2L, digits_effect = NULL, digits_pcons = NULL, digits_ci = NULL, include_dominated = FALSE, include_factor_columns = TRUE, highlight_selected = TRUE, ci_table = NULL )pareto_frontier_table( fs, format = c("gt", "data.table"), digits = 2L, digits_effect = NULL, digits_pcons = NULL, digits_ci = NULL, include_dominated = FALSE, include_factor_columns = TRUE, highlight_selected = TRUE, ci_table = NULL )
fs |
A |
format |
Character. Either |
digits |
Integer. Master decimal-places setting that drives
all three column-specific defaults ( |
digits_effect |
Integer or |
digits_pcons |
Integer or |
digits_ci |
Integer or |
include_dominated |
Logical. If |
include_factor_columns |
Logical. If |
highlight_selected |
Logical. If |
ci_table |
Optional data.table of frontier CIs from
|
The frontier is a diagnostic: it lists candidate subgroups that are
not dominated on (effect, N) simultaneously. It is computed
inside sg_consistency_out (see
compute_pareto_frontier) and attached to the result
object as fs$grp.consistency$out_sg$pareto_frontier. It is
not used for selection – the selected subgroup is chosen
by sg_focus and may or may not appear on the frontier.
Scale handling. For ratio measures stored on the log
scale internally ("OR", "RR", "IRR"), the
effect column is exponentiated for display. For "HR" (Cox
PH, natural scale) and identity-scale measures ("RD",
"IRD", "MD"), values pass through unchanged.
Selected-row identification. The selected subgroup is
identified by matching the original-table row index m
against the top row of fs$grp.consistency$out_sg$result.
This is robust to sorting and outcome-type differences.
For sg_focus = "hrMinSG", the selected subgroup may
not appear on the frontier – that focus deliberately prefers small
subgroups, which are typically N-dominated.
Optional confidence intervals. When ci_table is
supplied (the data.table returned by
compute_frontier_cis), three CI columns are added to
the table. All three CIs are computed by
compute_frontier_cis; this function only displays
what is passed in. Pass the same ci_table to
plot_pareto_frontier for consistent display across
table and plot.
Naive 95% CI – full-sample Wald CI from a Cox or
GLM refit on each frontier member's data. Ignores
subgroup-search selection; anti-conservative by construction.
Split ~ 95% CI – subsample-derived approximation,
computed from the empirical SD of averaged half-sample effects
across the splits used by compute_frontier_cis.
FSBC ~ 95% CI – bias-corrected interval following
the bootstrap algorithm of Leon2024fs (eq 7) but
treating the selected subgroup as fixed across half-jackknife
replicates. The cell shows est (lcl, ucl) where
est is the bias-corrected effect estimate
.
See compute_frontier_cis for the algebra.
Depending on format:
"gt"A gt_tbl object. Returns NULL
invisibly (with a message) if the frontier is unavailable or
empty.
"data.table"A data.table of the frontier
with effect on the natural scale, an is_selected
logical column, and effect column renamed to the
effect_measure label (e.g., "HR", "OR").
Returns an empty data.table if the frontier is
unavailable.
compute_pareto_frontier,
compute_frontier_cis,
plot_pareto_frontier,
sort_subgroups, forestsearch.
## Not run: # Survival example data(gbsg, package = "survival") fs <- forestsearch(gbsg, ...) # Basic table (no CIs) pareto_frontier_table(fs) pareto_frontier_table(fs, format = "data.table") # With CIs (compute once, display anywhere) ci_tab <- compute_frontier_cis(fs, n_splits = 1000, seed = 1) pareto_frontier_table(fs, ci_table = ci_tab) plot_pareto_frontier(fs, ci_table = ci_tab) # same intervals ## End(Not run)## Not run: # Survival example data(gbsg, package = "survival") fs <- forestsearch(gbsg, ...) # Basic table (no CIs) pareto_frontier_table(fs) pareto_frontier_table(fs, format = "data.table") # With CIs (compute once, display anywhere) ci_tab <- compute_frontier_cis(fs, n_splits = 1000, seed = 1) pareto_frontier_table(fs, ci_table = ci_tab) plot_pareto_frontier(fs, ci_table = ci_tab) # same intervals ## End(Not run)
Creates a visualization of the detection probability curve.
plot_detection_curve( curve_data, add_reference_lines = TRUE, add_threshold_line = TRUE, title = NULL, ... )plot_detection_curve( curve_data, add_reference_lines = TRUE, add_threshold_line = TRUE, title = NULL, ... )
curve_data |
A data.frame from |
add_reference_lines |
Logical. Add horizontal reference lines at 0.05, 0.10, 0.80. Default: TRUE |
add_threshold_line |
Logical. Add vertical line at hr_threshold. Default: TRUE |
title |
Character. Plot title. Default: auto-generated |
... |
Additional arguments passed to plot() |
Invisibly returns the input data.
## Not run: curve_data <- generate_detection_curve(n_sg = 60, prop_cens = 0.2) plot_detection_curve(curve_data) ## End(Not run)## Not run: curve_data <- generate_detection_curve(n_sg = 60, prop_cens = 0.2) plot_detection_curve(curve_data) ## End(Not run)
Builds a three-panel violin plot (ITT / identified /
identified ) of treatment-effect estimates across
simulation replicates produced by
run_simulation_analysis(). Handles both GLM endpoints
(OR, RD, IRR) and survival (HR), optionally inverts effects to a
benefit scale (subgroup_notation = "benefit"), and overlays a
dotted reference line at the DGM-implied true effect in .
plot_effect_distribution( results, analysis_method = "FS", effect_measure = c("OR", "HR", "RD", "IRR", "MD"), subgroup_notation = c("harm", "benefit"), reference_effect = NA_real_, title = NULL, subtitle = NULL, panel_labels = NULL, drop_undetected = TRUE, trim_threshold = 1000, trim_fraction = 0.01, ylim = NULL )plot_effect_distribution( results, analysis_method = "FS", effect_measure = c("OR", "HR", "RD", "IRR", "MD"), subgroup_notation = c("harm", "benefit"), reference_effect = NA_real_, title = NULL, subtitle = NULL, panel_labels = NULL, drop_undetected = TRUE, trim_threshold = 1000, trim_fraction = 0.01, ylim = NULL )
results |
|
analysis_method |
Character. Method label in the
|
effect_measure |
Character. One of |
subgroup_notation |
Character. |
reference_effect |
Numeric or |
title |
Character. Plot title. Default: auto-constructed. |
subtitle |
Character or |
panel_labels |
Named character vector with names |
drop_undetected |
Logical. Restrict the |
trim_threshold |
Numeric or |
trim_fraction |
Numeric in (0, 0.5). Fraction of observations
to trim from each tail of each group when trimming triggers.
Default: |
ylim |
Numeric vector of length 2 or |
The MRCT analogue is SGplot_estimates, which works on
the four-panel mrct_region_sims() schema (ITT / training /
testing / subgroup). This function targets the three-panel
run_simulation_analysis() schema that does not involve a
train/test split.
A ggplot2 object. Has attr(p, "panel_data")
set to the long-format data.table used for plotting (for
downstream summaries or diagnostic inspection). When trimming is
active, also has attr(p, "trim_info") containing per-group
diagnostics (n_total, n_trimmed, n_flagged,
raw_mean, raw_sd, trimmed_mean,
trimmed_sd, lower_bound, upper_bound).
run_simulation_analysis for the simulation
pipeline, SGplot_estimates for the MRCT analogue,
build_estimation_table for the tabular counterpart.
## Not run: # Binary endpoint, benefit search, OR scale true_or_benefit <- 1 / dgm_calibrated$hazard_ratios$harm_subgroup p <- plot_effect_distribution( results_alt, analysis_method = "FS", effect_measure = "OR", subgroup_notation = "benefit", reference_effect = true_or_benefit, title = "ACTG175 Binary (HTE): OR Estimates Across Simulations", subtitle = sprintf("n = 1000, 50 sims | truth OR(Q) = %.2f", true_or_benefit) ) print(p) # Survival endpoint, harm search, HR scale (no inversion) p_surv <- plot_effect_distribution( results_alt, effect_measure = "HR", subgroup_notation = "harm", reference_effect = dgm_alt$hazard_ratios$harm_subgroup ) # Clip y-axis to focus on the interesting OR range p_clipped <- plot_effect_distribution( results_alt, effect_measure = "OR", subgroup_notation = "benefit", reference_effect = true_or_benefit, ylim = c(0, 4) ) ## End(Not run)## Not run: # Binary endpoint, benefit search, OR scale true_or_benefit <- 1 / dgm_calibrated$hazard_ratios$harm_subgroup p <- plot_effect_distribution( results_alt, analysis_method = "FS", effect_measure = "OR", subgroup_notation = "benefit", reference_effect = true_or_benefit, title = "ACTG175 Binary (HTE): OR Estimates Across Simulations", subtitle = sprintf("n = 1000, 50 sims | truth OR(Q) = %.2f", true_or_benefit) ) print(p) # Survival endpoint, harm search, HR scale (no inversion) p_surv <- plot_effect_distribution( results_alt, effect_measure = "HR", subgroup_notation = "harm", reference_effect = dgm_alt$hazard_ratios$harm_subgroup ) # Clip y-axis to focus on the interesting OR range p_clipped <- plot_effect_distribution( results_alt, effect_measure = "OR", subgroup_notation = "benefit", reference_effect = true_or_benefit, ylim = c(0, 4) ) ## End(Not run)
Reproduces the supplement's involvement figure: the share of detections whose rule names the anchor, the true partner, and the proxy, against the sweep dimension.
plot_fs_identification_involvement(x, roles = NULL, palette = NULL)plot_fs_identification_involvement(x, roles = NULL, palette = NULL)
x |
An |
roles |
Character vector. Which covariates to draw; defaults to the anchor/partner/proxy triple. |
palette |
Named character vector of colours, or |
A ggplot object.
Reproduces the supplement's stacked-composition figure.
plot_fs_identification_structure(x, min_label = 0.03, palette = NULL)plot_fs_identification_structure(x, min_label = 0.03, palette = NULL)
x |
An |
min_label |
Numeric. Shares below this are drawn without a label. |
palette |
Named character vector of fills, or |
A ggplot object.
Bar-chart companion to fs_rule_covariate_pairs(), in the same idiom as the
selected-covariate figure: horizontal bars, ordered by share, coloured by
how the signature relates to the true region.
plot_fs_rule_pairs( x, true_covariates = NULL, top_n = 12L, detected_only = TRUE )plot_fs_rule_pairs( x, true_covariates = NULL, top_n = 12L, detected_only = TRUE )
x |
An |
true_covariates |
Character vector; taken from |
top_n |
Integer. Show only the most frequent |
detected_only |
Passed through. |
A ggplot object.
Creates Kaplan-Meier survival difference band plots comparing the identified
ForestSearch subgroup (sg.harm) and its complement against the ITT population.
This function wraps plotKM.band_subgroups() from the weightedsurv
package, automatically extracting subgroup definitions from ForestSearch
results.
plot_km_band_forestsearch( df, fs.est = NULL, sg_cols = NULL, sg_labels = NULL, sg_colors = NULL, itt_color = "azure3", outcome.name = "tte", event.name = "event", treat.name = "treat", xlabel = "Time", ylabel = "Survival differences", yseq_length = 5, draws_band = 1000, tau_add = NULL, by_risk = 6, risk_cex = 0.75, risk_delta = 0.035, risk_pad = 0.015, ymax_pad = 0.11, show_legend = TRUE, legend_pos = "topleft", legend_cex = 0.75, ref_subgroups = NULL, verbose = FALSE )plot_km_band_forestsearch( df, fs.est = NULL, sg_cols = NULL, sg_labels = NULL, sg_colors = NULL, itt_color = "azure3", outcome.name = "tte", event.name = "event", treat.name = "treat", xlabel = "Time", ylabel = "Survival differences", yseq_length = 5, draws_band = 1000, tau_add = NULL, by_risk = 6, risk_cex = 0.75, risk_delta = 0.035, risk_pad = 0.015, ymax_pad = 0.11, show_legend = TRUE, legend_pos = "topleft", legend_cex = 0.75, ref_subgroups = NULL, verbose = FALSE )
df |
Data frame. The analysis dataset containing all required variables including subgroup indicator columns. |
fs.est |
A forestsearch object containing the identified subgroup,
or |
sg_cols |
Character vector. Names of columns in |
sg_labels |
Character vector. Subsetting expressions for each subgroup,
corresponding to |
sg_colors |
Character vector. Colors for each subgroup curve,
corresponding to |
itt_color |
Character. Color for ITT population band.
Default: |
outcome.name |
Character. Name of time-to-event column.
Default: |
event.name |
Character. Name of event indicator column.
Default: |
treat.name |
Character. Name of treatment column.
Default: |
xlabel |
Character. X-axis label. Default: |
ylabel |
Character. Y-axis label. Default: |
yseq_length |
Integer. Number of y-axis tick marks.
Default: |
draws_band |
Integer. Number of bootstrap draws for confidence band.
Default: |
tau_add |
Numeric. Time horizon for the plot. If |
by_risk |
Numeric. Interval for risk table. Default: |
risk_cex |
Numeric. Character expansion for risk table text.
Default: |
risk_delta |
Numeric. Vertical spacing for risk table.
Default: |
risk_pad |
Numeric. Padding for risk table. Default: |
ymax_pad |
Numeric. Y-axis maximum padding. Default: |
show_legend |
Logical. Whether to display the legend.
Default: |
legend_pos |
Character. Legend position (e.g., "topleft", "bottomright").
Default: |
legend_cex |
Numeric. Character expansion for legend text.
Default: |
ref_subgroups |
Named list. Optional additional reference subgroups to include. Each element should be a list with:
The function automatically creates indicator columns from the expressions.
Default: |
verbose |
Logical. Print diagnostic messages. Default: |
This function simplifies the workflow of creating KM survival difference band plots for ForestSearch-identified subgroups. It can work in two modes:
Mode 1: With ForestSearch result (fs.est provided)
Extracts the subgroup definition from the ForestSearch result
Creates binary indicator columns (Qrecommend, Brecommend) in df
Generates appropriate labels from the subgroup definition
Calls plotKM.band_subgroups() with configured parameters
Mode 2: With pre-defined columns (sg_cols provided)
Uses existing indicator columns in df
Requires sg_labels and sg_colors to match sg_cols
The sg.harm subgroup (Qrecommend) represents patients with questionable
treatment benefit (where treat.recommend == 0 in ForestSearch output).
The complement (Brecommend) represents patients recommended for treatment.
Invisibly returns a list containing:
The modified data frame with subgroup indicators
Character vector of subgroup column names used
Character vector of subgroup labels used
Character vector of colors used
The subgroup definition extracted from fs.est
The reference subgroups list (if provided)
When fs.est is provided, the subgroup definition is extracted from:
fs.est$grp.consistency$out_sg$sg.harm_label - Human-readable labels
fs.est$sg.harm - Technical factor names (fallback)
fs.est$df.est$treat.recommend - Subgroup membership indicator
This function requires the weightedsurv package, which can be
installed from CRAN: install.packages("weightedsurv")
forestsearch for running the subgroup analysis
plot_sg_weighted_km for weighted KM plots
plot_sg_results for comprehensive subgroup visualization
## Not run: # Mode 1: Using ForestSearch result (auto-creates Qrecommend/Brecommend) # This will use labels "Qrecommend == 1" and "Brecommend == 1" plot_km_band_forestsearch( df = df.analysis, fs.est = fs_result, outcome.name = "os_months", event.name = "os_event", treat.name = "treatment" ) # Mode 1 with additional reference subgroups (auto-creates columns) ref_sgs <- list( age_young = list(subset_expr = "age < 65", color = "brown"), age_old = list(subset_expr = "age >= 65", color = "grey") ) plot_km_band_forestsearch( df = df.analysis, fs.est = fs_result, ref_subgroups = ref_sgs, outcome.name = "os_months", event.name = "os_event", treat.name = "treatment", tau_add = 60, by_risk = 6 ) # Mode 2: Using pre-defined subgroup columns with expression labels # Note: sg_labels must be valid R expressions that evaluate against df df.analysis$age_lt65 <- ifelse(df.analysis$age < 65, 1, 0) df.analysis$age_ge65 <- ifelse(df.analysis$age >= 65, 1, 0) df.analysis$Qrecommend <- ifelse(df.analysis$er <= 0, 1, 0) df.analysis$Brecommend <- ifelse(df.analysis$er > 0, 1, 0) plot_km_band_forestsearch( df = df.analysis, sg_cols = c("age_lt65", "age_ge65", "Qrecommend", "Brecommend"), sg_labels = c("age < 65", "age >= 65", "er <= 0", "er > 0"), sg_colors = c("brown", "grey", "blue", "red"), outcome.name = "os_months", event.name = "os_event", treat.name = "treatment", tau_add = 60, by_risk = 6 ) ## End(Not run)## Not run: # Mode 1: Using ForestSearch result (auto-creates Qrecommend/Brecommend) # This will use labels "Qrecommend == 1" and "Brecommend == 1" plot_km_band_forestsearch( df = df.analysis, fs.est = fs_result, outcome.name = "os_months", event.name = "os_event", treat.name = "treatment" ) # Mode 1 with additional reference subgroups (auto-creates columns) ref_sgs <- list( age_young = list(subset_expr = "age < 65", color = "brown"), age_old = list(subset_expr = "age >= 65", color = "grey") ) plot_km_band_forestsearch( df = df.analysis, fs.est = fs_result, ref_subgroups = ref_sgs, outcome.name = "os_months", event.name = "os_event", treat.name = "treatment", tau_add = 60, by_risk = 6 ) # Mode 2: Using pre-defined subgroup columns with expression labels # Note: sg_labels must be valid R expressions that evaluate against df df.analysis$age_lt65 <- ifelse(df.analysis$age < 65, 1, 0) df.analysis$age_ge65 <- ifelse(df.analysis$age >= 65, 1, 0) df.analysis$Qrecommend <- ifelse(df.analysis$er <= 0, 1, 0) df.analysis$Brecommend <- ifelse(df.analysis$er > 0, 1, 0) plot_km_band_forestsearch( df = df.analysis, sg_cols = c("age_lt65", "age_ge65", "Qrecommend", "Brecommend"), sg_labels = c("age < 65", "age >= 65", "er <= 0", "er > 0"), sg_colors = c("brown", "grey", "blue", "red"), outcome.name = "os_months", event.name = "os_event", treat.name = "treatment", tau_add = 60, by_risk = 6 ) ## End(Not run)
For two or more forestsearch fits whose consistency-passing
candidate sets are identical, produces a single Pareto-frontier
plot annotated with one S<i>: <combo_label> marker per
configuration at each selected subgroup. If two configurations pick
the same subgroup, the markers stack into a single multi-line label
(e.g.\ "S1: hrMaxSG/pareto\nS2: hrMaxSG/both").
plot_pareto_combined( fs_list, combo_labels = NULL, ci_table_list = NULL, show_band = TRUE, xlim = NULL, tolerance = 1e-06, verbose = TRUE )plot_pareto_combined( fs_list, combo_labels = NULL, ci_table_list = NULL, show_band = TRUE, xlim = NULL, tolerance = 1e-06, verbose = TRUE )
fs_list |
A list of |
combo_labels |
Character vector of length |
ci_table_list |
Optional list of CI tables from
|
show_band |
Logical. Draw the effect-band shading if applicable.
Default |
xlim |
Numeric vector of length 2 or |
tolerance |
Numeric. Per-cell tolerance for the value
equality check ( |
verbose |
Logical. Emit a warning naming the specific
equality-check failure mode when the sets don't match. Default
|
The combined plot is meaningful only when the passing sets match.
When they don't, the function returns NULL with a warning;
the side-by-side view (one panel per configuration) is the
appropriate alternative.
A ggplot object, or NULL (with a warning) if
the passing sets don't satisfy the equality criterion.
Two passing sets are considered equal when, after sorting by subgroup definition string:
they have the same number of rows;
the same set of subgroup definitions (concatenated
M.* columns), compared as sets (order-independent);
within each matched definition, the hr, N,
E, and K values agree within tolerance.
Pcons is deliberately excluded from the value check. It can
legitimately drift across rules because the preview sort (which
depends on selection_rule) changes the queue order, which
changes the random-split state each candidate consumes. Drift of
up to ~0.10 between runs on the same subgroup is expected and does
not indicate a real disagreement. Note also that the internal
candidate id m can differ across configurations for the
same reason; m is NOT used for equality.
compare_selection_rules,
plot_pareto_frontier,
pareto_frontier_table.
## Not run: out <- compare_selection_rules( df.analysis = actg_df, sg_focus = c("hrMaxSG", "hrMaxSG"), selection_rule = c("pareto", "both"), ... ) # Auto-attached by compare_selection_rules(): print(out$plot_combined) # Or call directly: p <- plot_pareto_combined( fs_list = out$fs, combo_labels = out$combos$label, ci_table_list = out$ci_tab ) print(p) ## End(Not run)## Not run: out <- compare_selection_rules( df.analysis = actg_df, sg_focus = c("hrMaxSG", "hrMaxSG"), selection_rule = c("pareto", "both"), ... ) # Auto-attached by compare_selection_rules(): print(out$plot_combined) # Or call directly: p <- plot_pareto_combined( fs_list = out$fs, combo_labels = out$combos$label, ci_table_list = out$ci_tab ) print(p) ## End(Not run)
Produces a 2D scatter of candidate subgroups in (effect, N) space,
with the Pareto frontier drawn as a step polyline and the selected
subgroup highlighted. When ci_table is supplied (the return
value of compute_frontier_cis), horizontal error bars
for the split-derived 95\
plot_pareto_frontier( fs, ci_table = NULL, show_band = FALSE, effect_neighborhood = NULL, label_members = TRUE, point_size = 3, xlim = NULL, xlim_trim = NULL )plot_pareto_frontier( fs, ci_table = NULL, show_band = FALSE, effect_neighborhood = NULL, label_members = TRUE, point_size = 3, xlim = NULL, xlim_trim = NULL )
fs |
A |
ci_table |
Optional |
show_band |
Logical. If |
effect_neighborhood |
Numeric. Override of the band width;
defaults to the value used in the original |
label_members |
Logical. If |
point_size |
Numeric. Size of frontier-member points.
Default |
xlim |
Optional numeric vector of length 2 controlling the
x-axis range, e.g.\ |
xlim_trim |
Logical or |
The frontier polyline is drawn as a downward step from large-effect
/ small-N candidates to small-effect / large-N candidates. Points
off the frontier (if any are present in the fs$grp.consistency$out_sg$result
table beyond those on the frontier) are not currently plotted; this
function only displays the frontier and its selected member.
For sg_focus = "hrMinSG", the selected subgroup may not lie
on the Pareto frontier (the focus deliberately prefers small
subgroups, which are typically N-dominated). In that case the
selected marker appears off the polyline.
A ggplot object.
pareto_frontier_table,
compute_frontier_cis, frontier_member_flags.
## Not run: p <- plot_pareto_frontier(fs) print(p) # With split-derived 95% CIs: ci_tab <- compute_frontier_cis(fs, n_splits = 1000) p2 <- plot_pareto_frontier(fs, ci_table = ci_tab) print(p2) ## End(Not run)## Not run: p <- plot_pareto_frontier(fs) print(p) # With split-derived 95% CIs: ci_tab <- compute_frontier_cis(fs, n_splits = 1000) p2 <- plot_pareto_frontier(fs, ci_table = ci_tab) print(p2) ## End(Not run)
True log-HR psi0(x) along x_pred with region histograms (paper Fig. 3 analogue)
plot_region_modifier(dgm, file = NULL, main = NULL)plot_region_modifier(dgm, file = NULL, main = NULL)
dgm |
An MRCT data-generating mechanism returned by
|
file |
Optional character path. When supplied, the plot is written
to this PNG file (900 x 800 px) and the device is closed on exit;
when |
main |
Optional character title for the upper panel. When |
NULL, invisibly. Called for its side effect of drawing a
two-panel plot.
Bar chart of subgroups identified across simulations, filtered to
those appearing in at least min_pct of the found simulations.
Supports two column schemas (MRCT and run_simulation_analysis),
top- capping with a pooled "Other" bar, automatic threshold
halving when no label clears the initial cut, and an n_bars
attribute for adaptive figure-height computation.
plot_sg_distribution( results, min_pct = 5, title = "Distribution of Identified Subgroups", wrap_width = 25, any_col = "any_found", label_col = "sg_found", top_k = NULL, show_other = TRUE, min_floor_pct = 0.5, max_halvings = 0L, bar_label_inside = FALSE, placeholder_on_empty = FALSE )plot_sg_distribution( results, min_pct = 5, title = "Distribution of Identified Subgroups", wrap_width = 25, any_col = "any_found", label_col = "sg_found", top_k = NULL, show_other = TRUE, min_floor_pct = 0.5, max_halvings = 0L, bar_label_inside = FALSE, placeholder_on_empty = FALSE )
results |
|
min_pct |
Numeric. Minimum percentage threshold for display
(0-100). Default: |
title |
Character. Plot title. |
wrap_width |
Integer. Character width for wrapping long
subgroup labels. Default: |
any_col |
Character. Column name of the per-replicate
detection flag. Default: |
label_col |
Character. Column name of the identified-subgroup
label string. Default: |
top_k |
Integer or |
show_other |
Logical. When |
min_floor_pct |
Numeric. Lower bound on automatic threshold
halving. Default: |
max_halvings |
Integer. Maximum number of halvings applied to
|
bar_label_inside |
Logical. Render bar labels inside bars
( |
placeholder_on_empty |
Logical. When no subgroups clear the
threshold (even after halving), return a minimal
"no data" placeholder plot with |
A ggplot2 object, or invisible(NULL) when no
subgroups are found and placeholder_on_empty = FALSE.
When a plot is returned it carries two attributes:
n_barsInteger. Number of bars in the plot
(0 in the placeholder case). Use
sgdist_fig_height() to compute an adaptive
figure height for knitr.
effective_min_pctNumeric. The threshold in effect after any automatic halving.
sgdist_fig_height for computing an adaptive
figure height from n_bars, and
run_simulation_analysis /
mrct_region_sims for the upstream simulation
engines.
## Not run: # MRCT / mrct_region_sims() output (default column schema) fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") plot_sg_distribution(fs$grp.consistency$result_new) # run_simulation_analysis() output with top-K cap and auto-halving fs_rows <- results_alt[results_alt$analysis == "FS", ] p <- plot_sg_distribution( fs_rows, any_col = "any.H", label_col = "sg.def", top_k = 15L, show_other = FALSE, max_halvings = 2L, bar_label_inside = TRUE, placeholder_on_empty = TRUE, title = "HTE: Subgroups Identified (H-hat)" ) fig_h <- sgdist_fig_height(attr(p, "n_bars")) ## End(Not run)## Not run: # MRCT / mrct_region_sims() output (default column schema) fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") plot_sg_distribution(fs$grp.consistency$result_new) # run_simulation_analysis() output with top-K cap and auto-halving fs_rows <- results_alt[results_alt$analysis == "FS", ] p <- plot_sg_distribution( fs_rows, any_col = "any.H", label_col = "sg.def", top_k = 15L, show_other = FALSE, max_halvings = 2L, bar_label_inside = TRUE, placeholder_on_empty = TRUE, title = "HTE: Subgroups Identified (H-hat)" ) fig_h <- sgdist_fig_height(attr(p, "n_bars")) ## End(Not run)
Creates a publication-ready visualization of identified subgroup outcomes
for binary or continuous endpoints. This is the GLM counterpart to
plot_sg_weighted_km for survival endpoints.
plot_sg_glm_outcomes( fs.est, fs_bc = NULL, outcome.name, treat.name, offset.name = NULL, effect_measure = NULL, outcome_type = NULL, adjust_covariates = NULL, sg0_name = "Questionable", sg1_name = "Recommend", E.name = "Treatment", C.name = "Control", bar_colors = c("steelblue", "coral"), show_effect = TRUE, show_bc = TRUE, show_itt = TRUE, conf.level = 0.95, title = NULL, subtitle = NULL, verbose = FALSE ) ## S3 method for class 'fs_binary_plot' print(x, ...) ## S3 method for class 'fs_binary_plot' plot(x, ...)plot_sg_glm_outcomes( fs.est, fs_bc = NULL, outcome.name, treat.name, offset.name = NULL, effect_measure = NULL, outcome_type = NULL, adjust_covariates = NULL, sg0_name = "Questionable", sg1_name = "Recommend", E.name = "Treatment", C.name = "Control", bar_colors = c("steelblue", "coral"), show_effect = TRUE, show_bc = TRUE, show_itt = TRUE, conf.level = 0.95, title = NULL, subtitle = NULL, verbose = FALSE ) ## S3 method for class 'fs_binary_plot' print(x, ...) ## S3 method for class 'fs_binary_plot' plot(x, ...)
fs.est |
A forestsearch object containing |
fs_bc |
Optional. Bootstrap results from
|
outcome.name |
Character. Name of outcome column. |
treat.name |
Character. Name of treatment column. |
offset.name |
Character or |
effect_measure |
Character or |
outcome_type |
Character or |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
sg0_name |
Character. Label for H subgroup (treat.recommend == 0).
Default: |
sg1_name |
Character. Label for Hc subgroup (treat.recommend == 1).
Default: |
E.name |
Character. Label for experimental/treatment arm.
Default: |
C.name |
Character. Label for control arm.
Default: |
bar_colors |
Character vector of length 2. Colors for control and
treatment bars. Default: |
show_effect |
Logical. Annotate with effect estimate. Default
|
show_bc |
Logical. Annotate with bias-corrected estimate when
|
show_itt |
Logical. Include an ITT (full population) panel.
Default |
conf.level |
Numeric. Confidence level for intervals.
Default |
title |
Character or |
subtitle |
Character or |
verbose |
Logical. Print diagnostic messages. Default |
x |
An fs_binary_plot object. |
... |
Additional arguments (unused). |
For binary outcomes, displays grouped bar charts of event rates by treatment arm within each subgroup. For continuous outcomes, displays point-and-errorbar charts of means (+/- SE) by treatment arm. Both are annotated with the effect estimate and optional bias-corrected estimate from the bootstrap.
A list of class "fs_binary_plot" containing:
A ggplot2 object.
Data frame of per-arm summaries used for the plot.
Data frame of effect estimates per subgroup.
Character. Subgroup definition label.
Character. Auto-generated figure note.
## Not run: fs <- forestsearch(df, outcome_type = "binary", effect_measure = "OR", ...) result <- plot_sg_glm_outcomes(fs, outcome.name = "status", treat.name = "hormon") print(result) ## End(Not run)## Not run: fs <- forestsearch(df, outcome_type = "binary", effect_measure = "OR", ...) result <- plot_sg_glm_outcomes(fs, outcome.name = "status", treat.name = "hormon") print(result) ## End(Not run)
Creates comprehensive visualizations of subgroup results from ForestSearch,
including Kaplan-Meier survival curves, hazard ratio comparisons, and
summary statistics. This function is designed to work with the output
from forestsearch, specifically the df.est component.
plot_sg_results( fs.est, outcome.name = "Y", event.name = "Event", treat.name = "Treat", plot_type = c("combined", "km", "forest", "summary"), by.risk = NULL, conf.level = 0.95, est.scale = c("hr", "1/hr"), sg0_name = "Questionable (H)", sg1_name = "Recommend (H^c)", treat_labels = c(`0` = "Control", `1` = "Treatment"), colors = NULL, title = NULL, show_events = TRUE, show_ci = TRUE, show_logrank = TRUE, show_hr = TRUE, verbose = FALSE, ... )plot_sg_results( fs.est, outcome.name = "Y", event.name = "Event", treat.name = "Treat", plot_type = c("combined", "km", "forest", "summary"), by.risk = NULL, conf.level = 0.95, est.scale = c("hr", "1/hr"), sg0_name = "Questionable (H)", sg1_name = "Recommend (H^c)", treat_labels = c(`0` = "Control", `1` = "Treatment"), colors = NULL, title = NULL, show_events = TRUE, show_ci = TRUE, show_logrank = TRUE, show_hr = TRUE, verbose = FALSE, ... )
fs.est |
A forestsearch object or list containing at minimum:
|
outcome.name |
Character. Name of time-to-event outcome column. Default: "Y" |
event.name |
Character. Name of event indicator column (1=event, 0=censored). Default: "Event" |
treat.name |
Character. Name of treatment column (1=treatment, 0=control). Default: "Treat" |
plot_type |
Character. Type of plot to create. One of:
|
by.risk |
Numeric. Risk interval for KM survival curves. Default: NULL (auto-calculated) |
conf.level |
Numeric. Confidence level for intervals. Default: 0.95 |
est.scale |
Character. Effect scale: "hr" (hazard ratio) or "1/hr" (inverse). Default: "hr" |
sg0_name |
Character. Label for subgroup 0 (harm/questionable). Default: "Questionable (H)" |
sg1_name |
Character. Label for subgroup 1 (recommend/complement). Default: "Recommend (H^c)" |
treat_labels |
Named character vector. Labels for treatment arms. Default: c("0" = "Control", "1" = "Treatment") |
colors |
Named character vector. Colors for plot elements. Default: uses package defaults |
title |
Character. Main plot title. Default: auto-generated |
show_events |
Logical. Show event counts on KM curves. Default: TRUE |
show_ci |
Logical. Show confidence intervals. Default: TRUE |
show_logrank |
Logical. Show log-rank p-value. Default: TRUE |
show_hr |
Logical. Show hazard ratio annotation. Default: TRUE |
verbose |
Logical. Print diagnostic messages. Default: FALSE |
... |
Additional arguments passed to plotting functions. |
The function extracts subgroup membership from fs.est$df.est$treat.recommend:
treat.recommend == 0: Harm/questionable subgroup (H)
treat.recommend == 1: Recommend/complement subgroup (H^c)
For est.scale = "1/hr", treatment labels and subgroup interpretation
are reversed to maintain clinical interpretability.
An object of class fs_sg_plot containing:
List of ggplot2 or base R plot objects
Data frame of subgroup summary statistics
Data frame of hazard ratio estimates
The matched call
When plot_type = "km", creates side-by-side survival curves for:
The identified subgroup (H) with treatment vs control
The complement subgroup (H^c) with treatment vs control
When plot_type = "forest", creates a forest plot showing hazard
ratios with confidence intervals for: ITT population, H subgroup,
and H^c complement.
forestsearch for running the subgroup analysis
sg_consistency_out for consistency evaluation
plot_subgroup_results_forestplot for publication-ready forest plots
## Not run: # Run ForestSearch analysis fs <- forestsearch( df.analysis = my_data, outcome.name = "os_time", event.name = "os_event", treat.name = "treatment", confounders.name = c("age_cat", "stage", "biomarker") ) # Create combined visualization result <- plot_sg_results( fs.est = fs, outcome.name = "os_time", event.name = "os_event", treat.name = "treatment" ) # View the Kaplan-Meier plots only plot_sg_results(fs, plot_type = "km") # Customize labels plot_sg_results( fs, sg0_name = "High Risk", sg1_name = "Standard Risk", treat_labels = c("0" = "Placebo", "1" = "Active Drug") ) ## End(Not run)## Not run: # Run ForestSearch analysis fs <- forestsearch( df.analysis = my_data, outcome.name = "os_time", event.name = "os_event", treat.name = "treatment", confounders.name = c("age_cat", "stage", "biomarker") ) # Create combined visualization result <- plot_sg_results( fs.est = fs, outcome.name = "os_time", event.name = "os_event", treat.name = "treatment" ) # View the Kaplan-Meier plots only plot_sg_results(fs, plot_type = "km") # Customize labels plot_sg_results( fs, sg0_name = "High Risk", sg1_name = "Standard Risk", treat_labels = c("0" = "Placebo", "1" = "Active Drug") ) ## End(Not run)
Creates weighted Kaplan-Meier survival curves for the identified subgroups
(H and Hc) using the weightedsurv package, matching the pattern used in
sg_consistency_out().
plot_sg_weighted_km( fs.est, fs_bc = NULL, outcome.name = "Y", event.name = "Event", treat.name = "Treat", by.risk = NULL, sg0_name = NULL, sg1_name = NULL, conf.int = TRUE, show.logrank = TRUE, show.cox = TRUE, show.cox.bc = TRUE, put.legend.lr = "topleft", ymax = 1.05, xmed.fraction = 0.65, hr_bc_position = "bottomright", hr_bc_cex = 0.725, title = NULL, verbose = FALSE )plot_sg_weighted_km( fs.est, fs_bc = NULL, outcome.name = "Y", event.name = "Event", treat.name = "Treat", by.risk = NULL, sg0_name = NULL, sg1_name = NULL, conf.int = TRUE, show.logrank = TRUE, show.cox = TRUE, show.cox.bc = TRUE, put.legend.lr = "topleft", ymax = 1.05, xmed.fraction = 0.65, hr_bc_position = "bottomright", hr_bc_cex = 0.725, title = NULL, verbose = FALSE )
fs.est |
A forestsearch object containing |
fs_bc |
Optional. Bootstrap results from |
outcome.name |
Character. Name of time-to-event column.
Default: |
event.name |
Character. Name of event indicator column.
Default: |
treat.name |
Character. Name of treatment column.
Default: |
by.risk |
Numeric. Risk interval for plotting. Default: |
sg0_name |
Character. Label for H subgroup (treat.recommend == 0).
Default: |
sg1_name |
Character. Label for Hc subgroup (treat.recommend == 1).
Default: |
conf.int |
Logical. Show confidence intervals. Default: |
show.logrank |
Logical. Show log-rank test. Default: |
show.cox |
Logical. Show unadjusted Cox HR from weightedsurv.
Default: |
show.cox.bc |
Logical. Show bootstrap bias-corrected HR annotation
(requires |
put.legend.lr |
Character. Legend position. Default: "topleft" |
ymax |
Numeric. Max y-axis value. Default: 1.05 |
xmed.fraction |
Numeric. Fraction for median lines. Default: 0.65 |
hr_bc_position |
Character. Position for bias-corrected HR annotation. One of "bottomright", "bottomleft", "topright", "topleft". Default: "bottomright" |
hr_bc_cex |
Numeric. Character expansion factor for bias-corrected HR annotation text. Default: 0.725 (matches weightedsurv cox.cex default) |
title |
Character. Overall plot title. Default: |
verbose |
Logical. Print diagnostic messages. Default: |
This function uses the exact same calling pattern as plot_subgroup()
in the ForestSearch package. Column names are mapped internally to the
standard names (Y, Event, Treat) expected by weightedsurv.
Subgroup definitions are automatically extracted from the forestsearch object if available:
fs$grp.consistency$out_sg$sg.harm_label - Human-readable labels
fs$sg.harm - Technical factor names (fallback)
HR display options controlled by show.cox and show.cox.bc:
Both TRUE (default): Shows unadjusted HR from weightedsurv AND bias-corrected HR annotation
show.cox = TRUE, show.cox.bc = FALSE: Shows only unadjusted HR
show.cox = FALSE, show.cox.bc = TRUE: Shows only bias-corrected HR
Both FALSE: Shows neither HR estimate
Invisibly returns a list with subgroup data frames and counting data
## Not run: # After running forestsearch - auto-extracts subgroup definition plot_sg_weighted_km(fs.est = fs) # With bootstrap bias-corrected estimates plot_sg_weighted_km(fs.est = fs, fs_bc = fs_bootstrap) # With custom column names plot_sg_weighted_km( fs.est = fs, outcome.name = "time_months", event.name = "status", treat.name = "hormon" ) ## End(Not run)## Not run: # After running forestsearch - auto-extracts subgroup definition plot_sg_weighted_km(fs.est = fs) # With bootstrap bias-corrected estimates plot_sg_weighted_km(fs.est = fs, fs_bc = fs_bootstrap) # With custom column names plot_sg_weighted_km( fs.est = fs, outcome.name = "time_months", event.name = "status", treat.name = "hormon" ) ## End(Not run)
Plot Spline Treatment Effect Function
plot_spline_treatment_effect(dgm_result, add_points = TRUE)plot_spline_treatment_effect(dgm_result, add_points = TRUE)
dgm_result |
Result object from generate_aft_dgm_flex with spline |
add_points |
Logical; add observed data points. Default TRUE |
Called for its side effect of producing a base-R plot. Returns
NULL invisibly.
## Not run: library(survival) df <- survival::gbsg dgm <- generate_aft_dgm_flex(df, outcome.name = "rfstime", event.name = "status", treat.name = "hormon", confounders.name = c("age", "meno", "nodes")) plot_spline_treatment_effect(dgm) ## End(Not run)## Not run: library(survival) df <- survival::gbsg dgm <- generate_aft_dgm_flex(df, outcome.name = "rfstime", event.name = "status", treat.name = "hormon", confounders.name = c("age", "meno", "nodes")) plot_spline_treatment_effect(dgm) ## End(Not run)
Creates diagnostic plots for subgroup treatment effects from df_super object
plot_subgroup_effects( df_super, z, hrz_crit = 0, log.hrs = NULL, ahr_empirical = NULL, plot_type = c("both", "profile", "ahr"), add_rug = TRUE, zpoints_by = 1, ... )plot_subgroup_effects( df_super, z, hrz_crit = 0, log.hrs = NULL, ahr_empirical = NULL, plot_type = c("both", "profile", "ahr"), add_rug = TRUE, zpoints_by = 1, ... )
df_super |
A data frame containing subgroup analysis results with columns: loghr_po (log hazard ratios), and optionally theta_1 and theta_0 (treatment-specific parameters) |
z |
Character string specifying the column name to use as the subgroup score (e.g., "z_age", "z_size", "subgroup"). Required. |
hrz_crit |
Critical hazard ratio threshold for defining optimal subgroup. Default is 1 (HR=1 on log scale is 0). |
log.hrs |
Optional vector of reference log hazard ratios to display as horizontal lines. Default is NULL. |
ahr_empirical |
Optional empirical average hazard ratio to display. If NULL, calculated from data. Default is NULL. |
plot_type |
Character string specifying plot type: "both" (default), "profile", or "ahr". |
add_rug |
Logical indicating whether to add rug plot of z values. Default is TRUE. |
zpoints_by |
Step size for z-axis grid when calculating AHR curves. Default is 1. |
... |
Additional graphical parameters passed to plot() |
The function creates up to two plots:
Treatment effect profile: Shows log hazard ratio as function of z
Average hazard ratio curve: Shows AHR for subgroups z >= threshold
The "optimal" subgroup is defined as patients with z >= cut.zero, where cut.zero is the minimum z value with favorable treatment effect (loghr < hrz_crit).
A list containing:
cut.zero |
The minimum z value where loghr_po < hrz_crit |
AHR_opt |
Average hazard ratio for optimal subgroup (z >= cut.zero) |
zpoints |
Grid of z values used for AHR calculations |
HR.zpoints |
AHR for population with z >= zpoints |
HRminus.zpoints |
AHR for population with z <= zpoints |
HR2.zpoints |
Alternative AHR calculation for z >= zpoints |
HRminus2.zpoints |
Alternative AHR calculation for z <= zpoints |
## Not run: # Using z_age as the subgroup score results <- plot_subgroup_effects(dgm_spline$df_super, z = "z_age", hrz_crit = 0) # Using subgroup identifier results <- plot_subgroup_effects(dgm_spline$df_super, z = "subgroup", hrz_crit = 0) # With reference lines results <- plot_subgroup_effects(dgm_spline$df_super, z = "z_size", hrz_crit = 0, log.hrs = c(-0.5, 0, 0.5)) # Only AHR plot results <- plot_subgroup_effects(dgm_spline$df_super, z = "z_pgr", plot_type = "ahr") ## End(Not run)## Not run: # Using z_age as the subgroup score results <- plot_subgroup_effects(dgm_spline$df_super, z = "z_age", hrz_crit = 0) # Using subgroup identifier results <- plot_subgroup_effects(dgm_spline$df_super, z = "subgroup", hrz_crit = 0) # With reference lines results <- plot_subgroup_effects(dgm_spline$df_super, z = "z_size", hrz_crit = 0, log.hrs = c(-0.5, 0, 0.5)) # Only AHR plot results <- plot_subgroup_effects(dgm_spline$df_super, z = "z_pgr", plot_type = "ahr") ## End(Not run)
Generates a comprehensive forest plot showing:
ITT (Intent-to-Treat) population estimate
Reference subgroups (e.g., by biomarker levels)
Post-hoc identified subgroups with bias-corrected estimates
Cross-validation agreement metrics as annotations
plot_subgroup_results_forestplot( fs_results, df_analysis, subgroup_list = NULL, outcome.name, event.name, treat.name, E.name = "Experimental", C.name = "Control", est.scale = "hr", xlog = TRUE, title_text = NULL, arrow_text = c("Favors Experimental", "Favors Control"), footnote_text = c("Eg 80% of training found SG: 70% of B (+) also B in CV testing"), xlim = c(0.25, 1.5), ticks_at = c(0.25, 0.7, 1, 1.5), show_cv_metrics = TRUE, cv_source = c("auto", "kfold", "oob", "both"), posthoc_colors = c("powderblue", "beige"), reference_colors = c("yellow", "powderblue"), ci_column_spaces = 20, conf.level = 0.95, theme = NULL, outcome_type = NULL, effect_measure = NULL, offset.name = NULL, adjust_covariates = NULL, extreme_ci_cap = 1.5, xlim_method = c("clinical", "data") )plot_subgroup_results_forestplot( fs_results, df_analysis, subgroup_list = NULL, outcome.name, event.name, treat.name, E.name = "Experimental", C.name = "Control", est.scale = "hr", xlog = TRUE, title_text = NULL, arrow_text = c("Favors Experimental", "Favors Control"), footnote_text = c("Eg 80% of training found SG: 70% of B (+) also B in CV testing"), xlim = c(0.25, 1.5), ticks_at = c(0.25, 0.7, 1, 1.5), show_cv_metrics = TRUE, cv_source = c("auto", "kfold", "oob", "both"), posthoc_colors = c("powderblue", "beige"), reference_colors = c("yellow", "powderblue"), ci_column_spaces = 20, conf.level = 0.95, theme = NULL, outcome_type = NULL, effect_measure = NULL, offset.name = NULL, adjust_covariates = NULL, extreme_ci_cap = 1.5, xlim_method = c("clinical", "data") )
fs_results |
List. A list containing ForestSearch analysis results with elements:
|
df_analysis |
Data frame. The analysis dataset with outcome, event, and treatment variables. |
subgroup_list |
List. Named list of subgroup definitions to include in the plot. Each element should be a list with:
|
outcome.name |
Character. Name of the survival time variable. |
event.name |
Character. Name of the event indicator variable. |
treat.name |
Character. Name of the treatment variable. |
E.name |
Character. Label for experimental arm (default: "Experimental"). |
C.name |
Character. Label for control arm (default: "Control"). |
est.scale |
Character. Estimate scale: "hr" or "1/hr" (default: "hr"). |
xlog |
Logical. If TRUE (default), the x-axis is plotted on a
logarithmic scale. This is standard for hazard ratio forest plots
where equal distances represent equal relative effects.
Implemented through forestploter's |
title_text |
Character. Plot title (default: NULL). |
arrow_text |
Character vector of length 2. Arrow labels for forest plot (default: c("Favors Experimental", "Favors Control")). |
footnote_text |
Character vector. Footnote text for the plot explaining CV metrics (default provides CV interpretation guidance; set to NULL to omit). |
xlim |
Numeric vector of length 2. X-axis limits (default: c(0.25, 1.5)). |
ticks_at |
Numeric vector. X-axis tick positions (default: c(0.25, 0.70, 1.0, 1.5)). |
show_cv_metrics |
Logical. Whether to show cross-validation metrics (default: TRUE if fs_kfold or fs_OOB available). |
cv_source |
Character. Source for CV metrics: "auto" (default, uses both if available, otherwise whichever is present), "kfold" (use fs_kfold only), "oob" (use fs_OOB only), or "both" (explicitly use both fs_kfold and fs_OOB, with K-fold first then OOB). |
posthoc_colors |
Character vector. Colors for post-hoc subgroup rows (default: c("powderblue", "beige")). |
reference_colors |
Character vector. Colors for reference subgroup rows (default: c("yellow", "powderblue")). |
ci_column_spaces |
Integer. Number of spaces for the CI plot column width. More spaces = wider CI column (default: 20). |
conf.level |
Numeric. Confidence level for intervals (default: 0.95 for 95% CI). Used to calculate the z-multiplier as qnorm(1 - (1 - conf.level)/2). |
theme |
An fs_forest_theme object from |
outcome_type |
Character or |
effect_measure |
Character or |
offset.name |
Character or |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
extreme_ci_cap |
Numeric. Multiplier for outlier-resistant axis
limits when |
xlim_method |
Character. Method for computing default axis limits
for GLM measures when |
Creates a publication-ready forest plot displaying identified subgroups with hazard ratios, bias-corrected estimates, and cross-validation metrics. This wrapper integrates ForestSearch results with the forestploter package.
ForestSearch identifies subgroups based on hazard ratio thresholds:
sg.harm: Contains the definition of the "harm" or "questionable"
subgroup (H)
treat.recommend == 0: Patient is IN the harm subgroup (H)
treat.recommend == 1: Patient is in the COMPLEMENT subgroup
(Hc, typically benefit)
For est.scale = "hr" (searching for harm):
H (treat.recommend=0): Subgroup defined by sg.harm with elevated HR (harm/questionable)
Hc (treat.recommend=1): Complement of sg.harm (potential benefit)
For est.scale = "1/hr" (searching for benefit):
Roles are reversed: H becomes the benefit group
A list containing:
The forestploter grob object (can be rendered with plot())
The data frame used for the forest plot
Character vector of row types for styling reference
Cross-validation metrics text (if available)
forestsearch for running the subgroup analysis
forestsearch_bootstrap_dofuture for bootstrap bias correction
forestsearch_Kfold for cross-validation
create_forest_theme for customizing plot appearance
render_forestplot for rendering the plot
## Not run: # Load ForestSearch results load("fs_analysis_results.Rdata") # Contains fs.est, fs_bc, fs_kfold # Define subgroups to display subgroups <- list( bm_gt1 = list( subset_expr = "BM > 1", name = "BM > 1", type = "reference" ), bm_gt1_size_gt19 = list( subset_expr = "BM > 1 & tmrsize > 19", name = "BM > 1 & Tumor Size > 19", type = "posthoc" ) ) # Create the forest plot with default theme result <- plot_subgroup_results_forestplot( fs_results = list(fs.est = fs.est, fs_bc = fs_bc, fs_kfold = fs_kfold), df_analysis = df_itt, subgroup_list = subgroups, outcome.name = "os_time", event.name = "os_event", treat.name = "combo", E.name = "Experimental+CT", C.name = "CT" ) # Create with custom theme for larger plot large_theme <- create_forest_theme( base_size = 14, row_padding = c(6, 4), cv_fontsize = 11 ) result <- plot_subgroup_results_forestplot( fs_results = list(fs.est = fs.est, fs_bc = fs_bc, fs_kfold = fs_kfold), df_analysis = df_itt, subgroup_list = subgroups, outcome.name = "os_time", event.name = "os_event", treat.name = "combo", theme = large_theme ) # Display the plot render_forestplot(result) ## End(Not run)## Not run: # Load ForestSearch results load("fs_analysis_results.Rdata") # Contains fs.est, fs_bc, fs_kfold # Define subgroups to display subgroups <- list( bm_gt1 = list( subset_expr = "BM > 1", name = "BM > 1", type = "reference" ), bm_gt1_size_gt19 = list( subset_expr = "BM > 1 & tmrsize > 19", name = "BM > 1 & Tumor Size > 19", type = "posthoc" ) ) # Create the forest plot with default theme result <- plot_subgroup_results_forestplot( fs_results = list(fs.est = fs.est, fs_bc = fs_bc, fs_kfold = fs_kfold), df_analysis = df_itt, subgroup_list = subgroups, outcome.name = "os_time", event.name = "os_event", treat.name = "combo", E.name = "Experimental+CT", C.name = "CT" ) # Create with custom theme for larger plot large_theme <- create_forest_theme( base_size = 14, row_padding = c(6, 4), cv_fontsize = 11 ) result <- plot_subgroup_results_forestplot( fs_results = list(fs.est = fs.est, fs_bc = fs_bc, fs_kfold = fs_kfold), df_analysis = df_itt, subgroup_list = subgroups, outcome.name = "os_time", event.name = "os_event", treat.name = "combo", theme = large_theme ) # Display the plot render_forestplot(result) ## End(Not run)
Dispatches to plot_sg_results for Kaplan-Meier curves,
hazard-ratio forest plots, or combined panels.
## S3 method for class 'forestsearch' plot( x, type = c("combined", "km", "forest", "summary"), outcome.name = "Y", event.name = "Event", treat.name = "Treat", ... )## S3 method for class 'forestsearch' plot( x, type = c("combined", "km", "forest", "summary"), outcome.name = "Y", event.name = "Event", treat.name = "Treat", ... )
x |
A |
type |
Character. Type of plot:
|
outcome.name |
Character. Name of time-to-event column.
Default: |
event.name |
Character. Name of event indicator column.
Default: |
treat.name |
Character. Name of treatment column.
Default: |
... |
Additional arguments passed to |
Invisibly returns the plot result from
plot_sg_results.
plot_sg_results for full control over appearance,
plot_sg_weighted_km for weighted KM curves,
plot_subgroup_results_forestplot for publication-ready
forest plots.
## Not run: fs <- forestsearch(df.analysis = mydata, ...) # Combined KM + forest plot (default) plot(fs) # KM curves only plot(fs, type = "km") # Forest plot only plot(fs, type = "forest") # With non-standard column names plot(fs, type = "km", outcome.name = "os_months", event.name = "os_event", treat.name = "treatment") # With custom labels plot(fs, sg0_name = "High Risk", sg1_name = "Standard Risk", treat_labels = c("0" = "Placebo", "1" = "Active Drug")) ## End(Not run)## Not run: fs <- forestsearch(df.analysis = mydata, ...) # Combined KM + forest plot (default) plot(fs) # KM curves only plot(fs, type = "km") # Forest plot only plot(fs, type = "forest") # With non-standard column names plot(fs, type = "km", outcome.name = "os_months", event.name = "os_event", treat.name = "treatment") # With custom labels plot(fs, sg0_name = "High Risk", sg1_name = "Standard Risk", treat_labels = c("0" = "Placebo", "1" = "Active Drug")) ## End(Not run)
Plot Method for ForestSearch Forest Plot
## S3 method for class 'fs_forestplot' plot(x, ...)## S3 method for class 'fs_forestplot' plot(x, ...)
x |
An fs_forestplot object |
... |
Additional arguments (ignored) |
The input x, invisibly.
## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") fp <- plot_subgroup_results_forestplot(list(fs.est = fs), gbsg, outcome.name = "rfstime", event.name = "status", treat.name = "hormon") plot(fp) ## End(Not run)## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") fp <- plot_subgroup_results_forestplot(list(fs.est = fs), gbsg, outcome.name = "rfstime", event.name = "status", treat.name = "hormon") plot(fp) ## End(Not run)
Plot Method for fs_sg_plot Objects
## S3 method for class 'fs_sg_plot' plot(x, which = 1, ...)## S3 method for class 'fs_sg_plot' plot(x, which = 1, ...)
x |
An fs_sg_plot object |
which |
Character or integer. Which plot to display. Default: 1 (first available) |
... |
Additional arguments passed to plot functions |
x, invisibly. No plot is drawn: a message names the
plot_sg_results() call that produces the requested plot. An error is
raised when a character which does not name an available plot.
## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") p <- plot_subgroup(fs) plot(p) ## End(Not run)## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") p <- plot_subgroup(fs) plot(p) ## End(Not run)
Draws one of the vignettes' six forest panels from a
summary.subgroup_sims() object via gg_forest(): the empirical-CI
distribution of the HR point estimate or of the upper 95% bound
UB(HR), for the single-variable panel, the combination + random-
benchmark panel, or the high-risk panel (Pr(UB>=2) filter with the
ITT row anchored first).
## S3 method for class 'subgroup_sims_summary' plot( x, metric = c("hr", "ub"), panel = c("single", "combo", "highrisk"), hr_true = x$hr_true, base_size = 14, point_size = 2.2, line_size = 0.75, widths = c(3.5, 5, 1.1, 1.2, 1.2, 1.1), clip_marker = "arrow", xlim = NULL, ticks_at = NULL, xlab = NULL, footnote = NULL, ... )## S3 method for class 'subgroup_sims_summary' plot( x, metric = c("hr", "ub"), panel = c("single", "combo", "highrisk"), hr_true = x$hr_true, base_size = 14, point_size = 2.2, line_size = 0.75, widths = c(3.5, 5, 1.1, 1.2, 1.2, 1.1), clip_marker = "arrow", xlim = NULL, ticks_at = NULL, xlab = NULL, footnote = NULL, ... )
x |
A |
metric |
|
panel |
|
hr_true |
True hazard ratio for the reference/vertical line and
footnotes; defaults to the value stored on |
base_size, point_size, line_size
|
Passed to |
widths |
Column width vector passed to |
clip_marker |
Passed to |
xlim, ticks_at, xlab, footnote
|
|
... |
Further arguments merged over the defaults and passed to
|
Defaults reproduce the committed vignette figures exactly:
metric-specific axis limits, tick positions, reference/vertical line
styling, the four annotation columns (N plus tail probabilities and
the unconditional median), and the panel-specific footnotes. Any
default can be overridden through the named arguments or ...,
which is merged over the assembled argument list before the
gg_forest() call (so e.g. ref_col = "black" or
widths = ... in ... win).
Summaries carrying effect metadata (the generic path of
summary.subgroup_sims(), e.g. from subgroup_glm() fits) take a
scale-aware branch instead: identity-scale measures (MD) plot with
xlog = FALSE and the null line at 0, ratio-scale measures (OR)
with xlog = TRUE and the null line at 1, and both delegate
limits/ticks to gg_forest()'s data-driven defaults (the HR panel
constants can hide binary medians and remain available via
xlim=/ticks_at=); annotation
columns and footnotes are built from the resolved labels and
threshold strings (legacy literals preserved when a pair equals the
legacy pair), and columns for disabled (NA) thresholds are dropped
(the default widths shrink to match; an explicit widths is used
as given). hr_true is interpreted on the fitter's estimate scale
throughout. Legacy summaries produce byte-identical gg_forest()
calls.
The gg_forest() plot object.
forest_height(), summary.subgroup_sims()
## Not run: S <- summary(sims_uniform, hr_true = 0.70) plot(S, metric = "ub", panel = "combo") ## End(Not run)## Not run: S <- summary(sims_uniform, hr_true = 0.70) plot(S, metric = "ub", panel = "combo") ## End(Not run)
Uses a calibrated model to predict the
procedure-level FPR at arbitrary sample sizes and thresholds.
predict_fpr_corrected( calibration, theta, d_eff, c1, c2, N, effect_scale = "ratio" )predict_fpr_corrected( calibration, theta, d_eff, c1, c2, N, effect_scale = "ratio" )
calibration |
An |
theta |
Numeric. True treatment effect (1.0 for null). |
d_eff |
Numeric. Effective information in the subgroup. |
c1 |
Numeric. Screening threshold. |
c2 |
Numeric. Consistency threshold. |
N |
Integer. Total sample size. |
effect_scale |
Character. |
A list with components:
Per-subgroup detection probability.
Effective number of candidates at this N.
Corrected procedure-level FPR.
cal <- calibrate_L_eff( N = c(200, 500, 700, 1000), P1 = c(0.152, 0.109, 0.091, 0.071), sim_fpr = c(0.185, 0.220, 0.270, 0.280) ) # Predict FPR at N = 800 d <- d_eff_binary(n_sg = round(0.30 * 800), p_event = 0.30) predict_fpr_corrected(cal, theta = 1.0, d_eff = d, c1 = 1.25, c2 = 1.25, N = 800)cal <- calibrate_L_eff( N = c(200, 500, 700, 1000), P1 = c(0.152, 0.109, 0.091, 0.071), sim_fpr = c(0.185, 0.220, 0.270, 0.280) ) # Predict FPR at N = 800 d <- d_eff_binary(n_sg = round(0.30 * 800), p_event = 0.30) predict_fpr_corrected(cal, theta = 1.0, d_eff = d, c1 = 1.25, c2 = 1.25, N = 800)
Returns the estimated treatment-effect contrast
, on the natural
parameter scale of the fitted family (mean difference for Gaussian, log
odds ratio for binomial, log mean ratio for Poisson, log hazard ratio for
Cox).
## S3 method for class 'dina' predict(object, newdata, ...)## S3 method for class 'dina' predict(object, newdata, ...)
object |
an object of class |
newdata |
a numeric matrix or data frame with the same number of
columns as the training |
... |
unused. |
numeric vector of predicted treatment-effect contrasts, one per
row of newdata.
Splits a data frame into two subgroups based on a flag and treatment scale.
prepare_subgroup_data(df, SG_flag, est.scale, treat.name)prepare_subgroup_data(df, SG_flag, est.scale, treat.name)
df |
Data frame. |
SG_flag |
Character. Name of subgroup flag variable. |
est.scale |
Character. Effect scale ("hr" or "1/hr"). |
treat.name |
Character. Name of treatment variable. |
List with subgroup data frames and treatment variable name.
df <- data.frame( treat = c(0, 1, 0, 1, 0), sg_flag = c(1, 1, 0, 0, 1) ) result <- prepare_subgroup_data(df, SG_flag = "sg_flag", est.scale = "hr", treat.name = "treat") nrow(result$df_1)df <- data.frame( treat = c(0, 1, 0, 1, 0), sg_flag = c(1, 1, 0, 0, 1) ) result <- prepare_subgroup_data(df, SG_flag = "sg_flag", est.scale = "hr", treat.name = "treat") nrow(result$df_1)
Displays detailed information about the GRF analysis
print_grf_details(config, values, best_subgroup, sg_harm_id, tree_cuts = NULL)print_grf_details(config, values, best_subgroup, sg_harm_id, tree_cuts = NULL)
config |
List. GRF configuration |
values |
Data frame. Node metrics |
best_subgroup |
Data frame row. Selected subgroup (or NULL) |
sg_harm_id |
Character. Subgroup definition (or NULL) |
tree_cuts |
List. Cut information |
Called for its side effect of printing to the console. The value
is returned invisibly and is incidental: tree_cuts$all when a subgroup,
its definition and tree_cuts are all supplied, otherwise NULL.
## Not run: # print_grf_details() is called internally by grf.subg.harm.survival(). # See grf.subg.harm.survival() for the standard entry point. ## End(Not run)## Not run: # print_grf_details() is called internally by grf.subg.harm.survival(). # See grf.subg.harm.survival() for the standard entry point. ## End(Not run)
Print method for cox_ahr_cde objects
## S3 method for class 'cox_ahr_cde' print(x, ...)## S3 method for class 'cox_ahr_cde' print(x, ...)
x |
A |
... |
Additional arguments (not used). |
Invisibly returns the input object.
## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") res <- cox_ahr_cde_analysis(df, outcome.name = "rfstime", event.name = "status", treat.name = "hormon", confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er")) print(res) ## End(Not run)## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") res <- cox_ahr_cde_analysis(df, outcome.name = "rfstime", event.name = "status", treat.name = "hormon", confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er")) print(res) ## End(Not run)
Extends print.dina() with bag count and per-bag cross-fitting info.
## S3 method for class 'dina_bagged' print(x, digits = max(3L, getOption("digits") - 3L), ...)## S3 method for class 'dina_bagged' print(x, digits = max(3L, getOption("digits") - 3L), ...)
x |
an object of class |
digits |
number of digits to show for coefficients. |
... |
unused. |
invisibly returns x.
dina_subgroup result.Print a dina_subgroup result.
## S3 method for class 'dina_subgroup' print(x, digits = max(3L, getOption("digits") - 3L), ...)## S3 method for class 'dina_subgroup' print(x, digits = max(3L, getOption("digits") - 3L), ...)
x |
a |
digits |
number of digits for numeric summary. |
... |
unused. |
invisibly returns x.
dina_subgroup_bootstrap result.Print a dina_subgroup_bootstrap result.
## S3 method for class 'dina_subgroup_bootstrap' print(x, digits = max(3L, getOption("digits") - 3L), max_select = 5L, ...)## S3 method for class 'dina_subgroup_bootstrap' print(x, digits = max(3L, getOption("digits") - 3L), max_select = 5L, ...)
x |
a |
digits |
number of digits for numeric summary. |
max_select |
maximum number of rows of the selection-frequency
table to display. Default |
... |
unused. |
invisibly returns x.
dina_subgroup_refit result.Print a dina_subgroup_refit result.
## S3 method for class 'dina_subgroup_refit' print(x, digits = max(3L, getOption("digits") - 3L), ...)## S3 method for class 'dina_subgroup_refit' print(x, digits = max(3L, getOption("digits") - 3L), ...)
x |
a |
digits |
number of digits for the numeric summary. |
... |
unused. |
invisibly returns x.
Displays a concise summary of ForestSearch results including the identified subgroup definition, consistency metrics, algorithm details, and computation time.
## S3 method for class 'forestsearch' print(x, ...)## S3 method for class 'forestsearch' print(x, ...)
x |
A |
... |
Additional arguments (currently unused). |
Invisibly returns x.
When the fit carries multiplier-resampling results (mr_inference = TRUE,
attached as x$mr_inference) and the field block ran
(ci_method = "field", the default), a further block reports: the
one-sided 95% lower bound for the identified subgroup (the field); the
one-sided 95% upper bound for its complement (field-s, the studentized
complement field); the Bonferroni pair for a joint two-subgroup
statement; the infinitesimal-jackknife (IJ) two-term two-sided interval,
as a secondary summary; and the re-selection frequency
as a descriptive diagnostic. Bounds are read by
their location relative to a clinically meaningful effect size, not as
tests at the null. With ci_method = "ij" only the two-sided interval is
shown; without MR results the output is unchanged. The constructions and
their simulation operating characteristics are described in León and
Anderson (2026), arXiv:2609.38361.
summary.forestsearch for detailed output,
plot.forestsearch for visualization.
## Not run: fs <- forestsearch(df.analysis = mydata, ...) print(fs) # or simply: fs ## End(Not run)## Not run: fs <- forestsearch(df.analysis = mydata, ...) print(fs) # or simply: fs ## End(Not run)
Short summary of the comparison. Use print(x$plot_grid) for
the side-by-side plot and cat(x$console[[i]]) for each
combo's diagnostic output.
## S3 method for class 'forestsearch_comparison' print(x, ...)## S3 method for class 'forestsearch_comparison' print(x, ...)
x |
A |
... |
Ignored. |
x, invisibly. Called for its side effect of printing the
comparison summary.
fpr_calibration objectsPrint method for fpr_calibration objects
## S3 method for class 'fpr_calibration' print(x, ...)## S3 method for class 'fpr_calibration' print(x, ...)
x |
An |
... |
Unused; present for S3 compatibility. |
The input x, invisibly.
Prints a concise but informative summary of an fs_bootstrap
object returned by forestsearch_bootstrap_dofuture.
Covers identification rate, agreement with the primary subgroup,
top identified subgroups, and bias-corrected effect estimates.
## S3 method for class 'fs_bootstrap' print(x, top_n = 3L, ...)## S3 method for class 'fs_bootstrap' print(x, top_n = 3L, ...)
x |
An |
top_n |
Integer. Maximum number of top identified subgroups to print. Default 3. |
... |
Additional arguments (ignored; present for S3 consistency). |
The print method is deliberately richer than the corresponding
print.fs_kfold / print.fs_tenfold methods because the
bootstrap is the primary inferential machinery of the package, while
the CV routines are complementary diagnostics. For the full
per-factor breakdown, consistency distribution, size distribution,
and GRF cut tabulation, call summarize_bootstrap_subgroups.
Invisibly returns x.
forestsearch_bootstrap_dofuture to produce the object.
summarize_bootstrap_subgroups for full tabulations.
## Not run: fs <- forestsearch( df.analysis = mydata, confounders.name = c("age", "sex", "biomarker"), outcome.name = "time", event.name = "status", treat.name = "treat" ) fs_bc <- forestsearch_bootstrap_dofuture(fs, nb_boots = 500) print(fs_bc) # rich default summary print(fs_bc, top_n = 5) ## End(Not run)## Not run: fs <- forestsearch( df.analysis = mydata, confounders.name = c("age", "sex", "biomarker"), outcome.name = "time", event.name = "status", treat.name = "treat" ) fs_bc <- forestsearch_bootstrap_dofuture(fs, nb_boots = 500) print(fs_bc) # rich default summary print(fs_bc, top_n = 5) ## End(Not run)
Print Method for fs_cv_summary Objects
## S3 method for class 'fs_cv_summary' print(x, n = 5L, ...)## S3 method for class 'fs_cv_summary' print(x, n = 5L, ...)
x |
An |
n |
Integer. Maximum rows to print from each raw data frame
(default: |
... |
Additional arguments (currently unused). |
Invisibly returns x.
fs_dgm_scale objectsPrint method for fs_dgm_scale objects
## S3 method for class 'fs_dgm_scale' print(x, ...)## S3 method for class 'fs_dgm_scale' print(x, ...)
x |
An object of class |
... |
Unused; present for S3 compatibility. |
The input x, invisibly.
fs_fdr_report objectsPrint method for fs_fdr_report objects
## S3 method for class 'fs_fdr_report' print(x, ...)## S3 method for class 'fs_fdr_report' print(x, ...)
x |
An |
... |
Unused; present for S3 compatibility. |
The input x, invisibly.
Print Method for ForestSearch Forest Theme
## S3 method for class 'fs_forest_theme' print(x, ...)## S3 method for class 'fs_forest_theme' print(x, ...)
x |
An fs_forest_theme object |
... |
Additional arguments (ignored) |
The input x, invisibly.
theme <- create_forest_theme() print(theme)theme <- create_forest_theme() print(theme)
Print Method for ForestSearch Forest Plot
## S3 method for class 'fs_forestplot' print(x, ...)## S3 method for class 'fs_forestplot' print(x, ...)
x |
An fs_forestplot object |
... |
Additional arguments (ignored) |
The input x, invisibly.
## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") fp <- plot_subgroup_results_forestplot(list(fs.est = fs), gbsg, outcome.name = "rfstime", event.name = "status", treat.name = "hormon") print(fp) ## End(Not run)## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") fp <- plot_subgroup_results_forestplot(list(fs.est = fs), gbsg, outcome.name = "rfstime", event.name = "status", treat.name = "hormon") print(fp) ## End(Not run)
Print Method for K-Fold CV Results
## S3 method for class 'fs_kfold' print(x, ...)## S3 method for class 'fs_kfold' print(x, ...)
x |
An fs_kfold object |
... |
Additional arguments (ignored) |
x, invisibly. Called for its side effect of printing the
K-fold summary.
## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") oob <- forestsearch_Kfold(fs.est = fs) print(oob) ## End(Not run)## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") oob <- forestsearch_Kfold(fs.est = fs) print(oob) ## End(Not run)
fs_mr_oc objectsPrint method for fs_mr_oc objects
## S3 method for class 'fs_mr_oc' print(x, ...)## S3 method for class 'fs_mr_oc' print(x, ...)
x |
An object of class |
... |
Unused; present for S3 compatibility. |
The input x, invisibly.
Print Method for fs_sg_plot Objects
## S3 method for class 'fs_sg_plot' print(x, ...)## S3 method for class 'fs_sg_plot' print(x, ...)
x |
An fs_sg_plot object |
... |
Additional arguments (unused) |
x, invisibly. Called for its side effect of printing the
subgroup definition, summary statistics and hazard ratio estimates.
## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") p <- plot_subgroup(fs) print(p) ## End(Not run)## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") p <- plot_subgroup(fs) print(p) ## End(Not run)
Print Method for Repeated K-Fold CV Results
## S3 method for class 'fs_tenfold' print(x, ...)## S3 method for class 'fs_tenfold' print(x, ...)
x |
An fs_tenfold object |
... |
Additional arguments (ignored) |
x, invisibly. Called for its side effect of printing the
repeated K-fold summary.
## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") kfold <- forestsearch_tenfold(fs.est = fs, sims = 5) print(kfold) ## End(Not run)## Not run: fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") kfold <- forestsearch_tenfold(fs.est = fs, sims = 5) print(kfold) ## End(Not run)
Print Method for fs_weighted_km Objects
## S3 method for class 'fs_weighted_km' print(x, ...)## S3 method for class 'fs_weighted_km' print(x, ...)
x |
An fs_weighted_km object from plot_sg_weighted_km() |
... |
Additional arguments (unused) |
The input x, invisibly.
Print Method for gbsg_dgm Objects
## S3 method for class 'gbsg_dgm' print(x, ...)## S3 method for class 'gbsg_dgm' print(x, ...)
x |
A gbsg_dgm object |
... |
Additional arguments (unused) |
x, invisibly. Called for its side effect of printing the
data-generating mechanism summary.
## Not run: dgm <- create_gbsg_dgm() print(dgm) ## End(Not run)## Not run: dgm <- create_gbsg_dgm() print(dgm) ## End(Not run)
glm_dgm objectsPrint method for glm_dgm objects
## S3 method for class 'glm_dgm' print(x, ...)## S3 method for class 'glm_dgm' print(x, ...)
x |
A |
... |
Unused; present for S3 compatibility. |
The input x, invisibly.
Print method for glm_effect_profile objects
## S3 method for class 'glm_effect_profile' print(x, ...)## S3 method for class 'glm_effect_profile' print(x, ...)
x |
A |
... |
Additional arguments (unused). |
Invisibly returns x.
Prints the family size, the selected candidate, the naive and de-biased
effects, and the three interval objects returned by guohe_algorithm3(),
labelled by their status (Guo-He bound / extension / heuristic).
## S3 method for class 'guohe_a3' print(x, ...)## S3 method for class 'guohe_a3' print(x, ...)
x |
An object of class |
... |
Ignored; present for method consistency. |
x, invisibly.
Prints the cross-validation objective over the r grid, marks the selected
value, and, when present, prints the refitted full-data
guohe_algorithm3() result.
## S3 method for class 'guohe_ar' print(x, ...)## S3 method for class 'guohe_ar' print(x, ...)
x |
An object of class |
... |
Ignored; present for method consistency. |
x, invisibly.
hte_test objectsPrint method for hte_test objects
## S3 method for class 'hte_test' print(x, ...)## S3 method for class 'hte_test' print(x, ...)
x |
An |
... |
Unused; present for S3 compatibility. |
The input x, invisibly.
leff_calibration objectsPrint method for leff_calibration objects
## S3 method for class 'leff_calibration' print(x, ...)## S3 method for class 'leff_calibration' print(x, ...)
x |
A |
... |
Unused; present for S3 compatibility. |
The input x, invisibly.
Print method for survreg_comparison objects
## S3 method for class 'multi_survreg_comparison' print(x, ...)## S3 method for class 'multi_survreg_comparison' print(x, ...)
x |
A survreg_comparison object |
... |
Additional arguments (not used) |
Invisibly returns the input object
## Not run: library(survival) df <- survival::gbsg res <- compare_multiple_survreg(df, outcome.name = "rfstime", event.name = "status") print(res) ## End(Not run)## Not run: library(survival) df <- survival::gbsg res <- compare_multiple_survreg(df, outcome.name = "rfstime", event.name = "status") print(res) ## End(Not run)
Evaluates a cut expression (e.g., "age <= mean(age)") and returns the expression with the value.
process_conf_force_expr(expr, df)process_conf_force_expr(expr, df)
expr |
Character string of the cut expression. |
df |
Data frame. |
Character string with evaluated value.
df <- data.frame(age = c(40, 55, 70, 35), size = c(20, 30, 25, 15)) process_conf_force_expr("age <= mean(age)", df) process_conf_force_expr("size <= median(size)", df)df <- data.frame(age = c(40, 55, 70, 35), size = c(20, 30, 25, 15)) process_conf_force_expr("age <= mean(age)", df) process_conf_force_expr("size <= median(size)", df)
Convenience wrapper with sensible defaults for quick visualization.
quick_km_band_plot(df, fs.est, outcome.name, event.name, treat.name, ...)quick_km_band_plot(df, fs.est, outcome.name, event.name, treat.name, ...)
df |
Data frame with analysis data. |
fs.est |
ForestSearch result object. |
outcome.name |
Character. Time-to-event column name. |
event.name |
Character. Event indicator column name. |
treat.name |
Character. Treatment column name. |
... |
Additional arguments passed to |
Invisibly returns the plot result.
## Not run: # Quick plot with minimal configuration quick_km_band_plot(df_analysis, fs_result, "os_months", "os_event", "treat") # With reference subgroups ref_sgs <- list( age_young = list(subset_expr = "age < 65", color = "brown"), age_old = list(subset_expr = "age >= 65", color = "grey") ) quick_km_band_plot(df_analysis, fs_result, "os_months", "os_event", "treat", ref_subgroups = ref_sgs, tau_add = 60) ## End(Not run)## Not run: # Quick plot with minimal configuration quick_km_band_plot(df_analysis, fs_result, "os_months", "os_event", "treat") # With reference subgroups ref_sgs <- list( age_young = list(subset_expr = "age < 65", color = "brown"), age_old = list(subset_expr = "age >= 65", color = "grey") ) quick_km_band_plot(df_analysis, fs_result, "os_months", "os_event", "treat", ref_subgroups = ref_sgs, tau_add = 60) ## End(Not run)
Renders a forest plot from plot_subgroup_results_forestplot().
render_forestplot(x, newpage = TRUE)render_forestplot(x, newpage = TRUE)
x |
An fs_forestplot object from |
newpage |
Logical. Call grid.newpage() before drawing. Default: TRUE. |
To control plot sizing, create a custom theme using create_forest_theme()
and pass it to plot_subgroup_results_forestplot():
my_theme <- create_forest_theme(base_size = 14, row_padding = c(6, 4))
result <- plot_subgroup_results_forestplot(..., theme = my_theme)
render_forestplot(result)
Invisibly returns the grob object.
## Not run: # For larger plot, use theme parameter in plot_subgroup_results_forestplot: large_theme <- create_forest_theme( base_size = 14, row_padding = c(6, 4), cv_fontsize = 11 ) result <- plot_subgroup_results_forestplot( fs_results = list(fs.est = fs, fs_bc = fs_bc), df_analysis = df.analysis, outcome.name = "time", event.name = "status", treat.name = "treatment", theme = large_theme ) render_forestplot(result) ## End(Not run)## Not run: # For larger plot, use theme parameter in plot_subgroup_results_forestplot: large_theme <- create_forest_theme( base_size = 14, row_padding = c(6, 4), cv_fontsize = 11 ) result <- plot_subgroup_results_forestplot( fs_results = list(fs.est = fs, fs_bc = fs_bc), df_analysis = df.analysis, outcome.name = "time", event.name = "status", treat.name = "treatment", theme = large_theme ) render_forestplot(result) ## End(Not run)
Converts a data frame of pre-computed reference simulation results (e.g.,
digitized from a published LaTeX table) into a styled gt table. This
is useful for displaying published benchmark results alongside new
simulation output within vignettes or reports.
render_reference_table( ref_df, title = "Reference Simulation Results", subtitle = NULL, bold_threshold = 0.05, subgroup_notation = c("harm", "benefit") )render_reference_table( ref_df, title = "Reference Simulation Results", subtitle = NULL, bold_threshold = 0.05, subgroup_notation = c("harm", "benefit") )
ref_df |
|
title |
Character. Table title. |
subtitle |
Character. Table subtitle. Default: |
bold_threshold |
Numeric. Values in |
subgroup_notation |
Character. |
A gt table object.
## Not run: ref <- data.frame( Scenario = "M1 Null: N=700", Metric = "any(H)", FS = 0.02, FSlg = 0.03, GRF = 0.25 ) render_reference_table(ref, title = "Reference Results") ## End(Not run)## Not run: ref <- data.frame( Scenario = "M1 Null: N=700", Metric = "any(H)", FS = 0.02, FSlg = 0.03, GRF = 0.25 ) render_reference_table(ref, title = "Reference Results") ## End(Not run)
Tears down the current future plan, forces garbage collection, optionally pauses, and spins up a fresh worker pool. Used to release worker-side memory between simulation phases (e.g. between alternative and null Monte Carlo runs) so that accumulated R-session state in long-running workers does not bloat memory or cause sporadic failures.
reset_workers( workers = NULL, strategy = "multisession", sleep = 1, gc = TRUE, quiet = FALSE )reset_workers( workers = NULL, strategy = "multisession", sleep = 1, gc = TRUE, quiet = FALSE )
workers |
Integer. Number of workers for the new pool.
Default |
strategy |
Character. The future strategy to
reinstate. Default |
sleep |
Numeric. Seconds to pause between teardown and
setup. Default |
gc |
Logical. Whether to force garbage collection between
teardown and setup. Default |
quiet |
Logical. Suppress informational |
The worker reset sequence is:
future::plan("sequential") – dismantles the
existing worker pool; R processes terminate and their memory
is returned to the OS.
gc(verbose = FALSE) – reclaims main-process memory
holding references to now-defunct futures.
Sys.sleep(sleep) – brief pause to let the OS
reclaim sockets / pipes before the new pool is requested.
A small non-zero sleep (the default 1) materially
reduces multisession setup failures observed on busy
Linux servers; set to 0 to skip.
future::plan(strategy, workers = workers) – starts
a fresh pool.
This function is a no-op if future is not installed or if
the requested strategy cannot be initialised; a warning is emitted
and the previous plan is left in place.
Invisibly, a list with the previous and current plan
class names: list(previous = <chr>, current = <chr>).
collect_results for error-tolerant
collection of foreach output.
if (requireNamespace("future", quietly = TRUE)) { future::plan("multisession", workers = 2L) # ... run alternative simulations ... reset_workers(workers = 2L) # ... run null simulations ... future::plan("sequential") }if (requireNamespace("future", quietly = TRUE)) { future::plan("multisession", workers = 2L) # ... run alternative simulations ... reset_workers(workers = 2L) # ... run null simulations ... future::plan("sequential") }
Calculates restricted mean survival time (RMST) for a subgroup.
rmst_calculation( df, tte.name = "tte", event.name = "event", treat.name = "treat" )rmst_calculation( df, tte.name = "tte", event.name = "event", treat.name = "treat" )
df |
Data frame. |
tte.name |
Character. Name of time-to-event variable. |
event.name |
Character. Name of event indicator variable. |
treat.name |
Character. Name of treatment variable. |
List with tau, RMST, RMST for treatment, RMST for control.
## Not run: library(survival) df <- data.frame( tte = gbsg$rfstime / 30.4375, event = gbsg$status, treat = gbsg$hormon ) rmst_calculation(df, tte.name = "tte", event.name = "event", treat.name = "treat") ## End(Not run)## Not run: library(survival) df <- data.frame( tte = gbsg$rfstime / 30.4375, event = gbsg$status, treat = gbsg$hormon ) rmst_calculation(df, tte.name = "tte", event.name = "event", treat.name = "treat") ## End(Not run)
Runs ForestSearch under the null hypothesis at multiple sample
sizes to calibrate , then returns a
calibration object for predicting corrected FPR.
run_null_calibration( sim_null_fn, fs_args, d_eff_fn, N_values = c(200, 500, 1000), c1 = 1.25, c2 = 1, n_sims = 100L, prop_harm = 0.3, n_min = 60L, seed_base = 42L, verbose = TRUE )run_null_calibration( sim_null_fn, fs_args, d_eff_fn, N_values = c(200, 500, 1000), c1 = 1.25, c2 = 1, n_sims = 100L, prop_harm = 0.3, n_min = 60L, seed_base = 42L, verbose = TRUE )
sim_null_fn |
Function. Generates one H0 dataset.
Signature: |
fs_args |
Named list. Arguments passed to |
d_eff_fn |
Function. Computes |
N_values |
Integer vector. Sample sizes to simulate.
At least 2 required. Default: |
c1 |
Numeric. Screening threshold for calibration. Default: 1.25. |
c2 |
Numeric. Consistency threshold for calibration. Default: 1.0. |
n_sims |
Integer. Simulations per sample size. Default: 100. |
prop_harm |
Numeric. Expected proportion in harm subgroup (for computing d_eff). Default: 0.30. |
n_min |
Integer. Minimum subgroup size. Default: 60. |
seed_base |
Integer. Base seed. Default: 42. |
verbose |
Logical. Print progress. Default: TRUE. |
For each N in N_values, the function:
Generates n_sims datasets via sim_null_fn()
Runs forestsearch() on each
Computes the simulated FPR = proportion with any H found
Computes via the GLM approximation
Calls calibrate_L_eff() to fit the power-law model
ForestSearch is run with
parallel_args = list(plan = "sequential") to avoid
nested parallelism. The outer simulation loop is sequential.
For faster execution, wrap in foreach %dofuture%
externally.
An "leff_calibration" object.
# Binary DGM under H0 sim_null <- function(n, seed) { set.seed(seed) data.frame( id = seq_len(n), treat = rbinom(n, 1, 0.5), bm1 = as.factor(rbinom(n, 1, 0.70)), bm2 = as.factor(rbinom(n, 1, 0.50)), age = round(rnorm(n, 55, 10)), ecog = as.factor(sample(0:1, n, TRUE, c(0.6, 0.4))), progressed = rbinom(n, 1, 0.30) ) } fs_args <- list( confounders.name = c("bm1", "bm2", "age", "ecog"), outcome.name = "progressed", event.name = "progressed", treat.name = "treat", id.name = "id", outcome_type = "binary", effect_measure = "OR", adverse_outcome = TRUE, pconsistency.threshold = 0.90, fs.splits = 200, n.min = 60, maxk = 2, use_lasso = FALSE, use_grf = FALSE, use_twostage = TRUE, is.RCT = TRUE, details = FALSE, plot.sg = FALSE, parallel_args = list(plan = "sequential", workers = 1, show_message = FALSE) ) # Illustrative size only (the function requires n_sims >= 10 and at least # two N values); a calibration for use takes n_sims of 50 or more over the # trial sizes of interest. cal <- run_null_calibration( sim_null_fn = sim_null, fs_args = fs_args, d_eff_fn = function(n) d_eff_binary(n, 0.30), N_values = c(200, 500), n_sims = 10, verbose = TRUE ) print(cal)# Binary DGM under H0 sim_null <- function(n, seed) { set.seed(seed) data.frame( id = seq_len(n), treat = rbinom(n, 1, 0.5), bm1 = as.factor(rbinom(n, 1, 0.70)), bm2 = as.factor(rbinom(n, 1, 0.50)), age = round(rnorm(n, 55, 10)), ecog = as.factor(sample(0:1, n, TRUE, c(0.6, 0.4))), progressed = rbinom(n, 1, 0.30) ) } fs_args <- list( confounders.name = c("bm1", "bm2", "age", "ecog"), outcome.name = "progressed", event.name = "progressed", treat.name = "treat", id.name = "id", outcome_type = "binary", effect_measure = "OR", adverse_outcome = TRUE, pconsistency.threshold = 0.90, fs.splits = 200, n.min = 60, maxk = 2, use_lasso = FALSE, use_grf = FALSE, use_twostage = TRUE, is.RCT = TRUE, details = FALSE, plot.sg = FALSE, parallel_args = list(plan = "sequential", workers = 1, show_message = FALSE) ) # Illustrative size only (the function requires n_sims >= 10 and at least # two N values); a calibration for use takes n_sims of 50 or more over the # trial sizes of interest. cal <- run_null_calibration( sim_null_fn = sim_null, fs_args = fs_args, d_eff_fn = function(n) d_eff_binary(n, 0.30), N_values = c(200, 500), n_sims = 10, verbose = TRUE ) print(cal)
General replacement for the legacy run_simulation_analysis() that
was coupled to simulate_from_gbsg_dgm() and GBSG-specific column
names. This version calls simulate_from_dgm and accepts
explicit column-name parameters, making it applicable to any DGM built
with generate_aft_dgm_flex.
run_simulation_analysis( sim_id, dgm, n_sample, analysis_time = Inf, cens_adjust = 0, max_follow = NULL, muC_adj = NULL, confounders_base = c("v1", "v2", "v3", "v4", "v5", "v6", "v7"), n_add_noise = 0L, outcome_name = "y_sim", event_name = "event_sim", treat_name = "treat_sim", harm_col = "flag_harm", run_fs = TRUE, run_fs_grf = TRUE, run_grf = TRUE, methods = NULL, fs_params = list(), grf_params = list(), cox_formula = NULL, cox_formula_adj = NULL, n_sims_total = NULL, seed_base = 8316951L, verbose = FALSE, verbose_n = NULL, debug = FALSE, keep = character(0), keep_first_n = Inf )run_simulation_analysis( sim_id, dgm, n_sample, analysis_time = Inf, cens_adjust = 0, max_follow = NULL, muC_adj = NULL, confounders_base = c("v1", "v2", "v3", "v4", "v5", "v6", "v7"), n_add_noise = 0L, outcome_name = "y_sim", event_name = "event_sim", treat_name = "treat_sim", harm_col = "flag_harm", run_fs = TRUE, run_fs_grf = TRUE, run_grf = TRUE, methods = NULL, fs_params = list(), grf_params = list(), cox_formula = NULL, cox_formula_adj = NULL, n_sims_total = NULL, seed_base = 8316951L, verbose = FALSE, verbose_n = NULL, debug = FALSE, keep = character(0), keep_first_n = Inf )
sim_id |
Integer. Simulation replicate index (used as seed offset). |
dgm |
An |
n_sample |
Integer. Per-replicate sample size. |
analysis_time |
Numeric. Calendar time of analysis on the DGM time
scale. Use |
cens_adjust |
Numeric. Log-scale shift to censoring times passed to
|
max_follow |
Deprecated. Use |
muC_adj |
Deprecated. Use |
confounders_base |
Character vector of base confounder names. |
n_add_noise |
Integer. Number of independent N(0,1) noise variables
to append. Default |
outcome_name |
Name of the observed time column in simulated data.
Default |
event_name |
Name of the event indicator column. Default
|
treat_name |
Name of the treatment column. Default |
harm_col |
Name of the true-subgroup indicator column. Default
|
run_fs |
Logical. Run ForestSearch (LASSO). Default |
run_fs_grf |
Logical. Run ForestSearch (LASSO + GRF). Default
|
run_grf |
Logical. Run standalone GRF. Default |
methods |
Optional specification of analysis arms that generalizes the
Each arm's overrides are merged on top of |
fs_params |
Named list of ForestSearch parameter overrides. Any
element of |
grf_params |
Named list of GRF parameter overrides. |
cox_formula |
Optional Cox formula for unadjusted ITT. |
cox_formula_adj |
Optional adjusted Cox formula. |
n_sims_total |
Integer. Total simulations (for progress messages). |
seed_base |
Integer. Base seed; replicate seed = |
verbose |
Logical. Print progress messages. Default |
verbose_n |
Integer. If set, only print for |
debug |
Logical. Print detailed debug output. Default |
keep |
Character vector. Optional names of heavy diagnostic
objects to attach as a list in
Default |
keep_first_n |
Integer. When |
A data.table with one row per analysis method.
Always-present scalar columns cover: true-subgroup size/effect
from the DGM, per-method detection flag (any.H), estimated
subgroup size (size.H, size.Hc), effect estimates
(hr.H.hat, hr.Hc.hat, hr.itt, and their true
counterparts), classification metrics against the DGM-stored
subgroup (sens, spec, ppv, npv), and
pairwise concordance between methods (agree.*.*,
kappa.*.*). As of v0.2.0 the following ForestSearch
diagnostic columns are also always populated (NA for GRF
rows):
sg.defCharacter. Cut-expression string of the
identified subgroup, formed as
paste(fs_full$sg.harm, collapse = " & "). Empty
string when no subgroup was identified. Read across all
replicates to diagnose which cuts the method is selecting
(e.g., table(results$sg.def)).
sg.n_factorsInteger. Number of factors in the identified subgroup definition.
n_candidates_evaluatedInteger. Number of candidate subgroups actually evaluated for consistency (before or at the early-stop candidate).
n_candidates_totalInteger. Total number of candidate subgroups available.
n_passedInteger. Number of candidates meeting the consistency threshold.
consistency_algorithmCharacter. "fixed"
or "twostage".
early_stop_triggeredLogical. Whether
evaluation stopped early due to stop_threshold.
fs_minutesNumeric. Wall time for the FS run in minutes.
The analogous GRF row carries sg.def (from
grf_result$sg.harm.id) and grf_selected_depth.
When keep is non-empty and sim_id <= keep_first_n,
the heavy objects are attached as
attr(result, "diagnostics") — a named list with one
element per keeper (fs_full, grf_full, etc.).
GLM-specific parameters (outcome_type, effect_measure,
offset.name) must be passed inside fs_params, not as
top-level arguments. They route only to the estimation step
(.extract_fs_estimates_gen, .extract_grf_estimates_gen),
not to forestsearch() itself, which uses Cox PH for subgroup
identification in v0.1.x. Passing these as top-level arguments will
result in them being silently ignored.
run_simulation_analysis() is designed to be called inside a
foreach() %dofuture% loop (one replicate per worker). In
that idiom, outer parallelism is provided by %dofuture%
(one replicate per worker) and the inner forestsearch()
call should run sequentially within each worker. Running both layers
multisession produces nested parallelism: each outer worker tries to
spawn its own pool of inner workers, which parallelly rejects
with a 300\
To prevent this, default_sim_params() sets
parallel_args = list(plan = "sequential") as the default for
the inner forestsearch() call. Users who call
run_simulation_analysis() once at the top level (e.g., for
interactive debugging) and want the inner pipeline to run in parallel
can opt back in by passing
fs_params = list(parallel_args = list(plan = "multisession", workers = N)).
simulate_from_dgm,
generate_aft_dgm_flex, setup_gbsg_dgm
## Not run: dgm <- setup_gbsg_dgm(model = "null", verbose = FALSE) # Inner forestsearch() runs sequentially by default; safe for both # interactive use and embedding inside foreach() %dofuture% loops. result <- run_simulation_analysis(sim_id = 1, dgm = dgm, n_sample = 500) # Opt back into inner multisession for a one-off top-level call: result <- run_simulation_analysis( sim_id = 1, dgm = dgm, n_sample = 500, fs_params = list(parallel_args = list(plan = "multisession", workers = 4)) ) ## End(Not run)## Not run: dgm <- setup_gbsg_dgm(model = "null", verbose = FALSE) # Inner forestsearch() runs sequentially by default; safe for both # interactive use and embedding inside foreach() %dofuture% loops. result <- run_simulation_analysis(sim_id = 1, dgm = dgm, n_sample = 500) # Opt back into inner multisession for a one-off top-level call: result <- run_simulation_analysis( sim_id = 1, dgm = dgm, n_sample = 500, fs_params = list(parallel_args = list(plan = "multisession", workers = 4)) ) ## End(Not run)
Performs one random 50/50 split and evaluates whether both halves meet the HR consistency threshold.
run_single_consistency_split( df.x, N.x, hr.consistency, cox_init = 0, estimator_fn = NULL, consistency_threshold = NULL, adjust_covariates = NULL )run_single_consistency_split( df.x, N.x, hr.consistency, cox_init = 0, estimator_fn = NULL, consistency_threshold = NULL, adjust_covariates = NULL )
df.x |
data.table. Subgroup data with columns Y, Event, Treat. |
N.x |
Integer. Number of observations in subgroup. |
hr.consistency |
Numeric. |
cox_init |
Numeric. Initial value for Cox model (log HR). |
estimator_fn |
Closure or |
consistency_threshold |
Numeric or |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
Numeric. 1 if both splits meet threshold, 0 if not, NA if error.
library(data.table) set.seed(1) df <- data.table( Y = rexp(100), Event = rbinom(100, 1, 0.55), Treat = rep(0:1, 50) ) run_single_consistency_split(df, N.x = 100, hr.consistency = 1.0)library(data.table) set.seed(1) df <- data.table( Y = rexp(100), Event = rbinom(100, 1, 0.55), Treat = rep(0:1, 50) ) run_single_consistency_split(df, N.x = 100, hr.consistency = 1.0)
Draws n_sims synthetic trials from a calibrated DGM via
simulate_from_dgm(), fits fit in every pre-defined subgroup of
every trial, and returns the raw result matrices plus full
provenance. With the default fit, benchmarks, seeds, and skip
rule, the matrices are bit-identical to those produced by the
extreme_subgroups vignettes for the matching baseline, and the
returned object is byte-compatible with their RDS payload:
saveRDS(sims, path) yields a file the design-comparison memo loads
unchanged.
run_subgroup_sims( dgm, subgroups, n_sims, fit = subgroup_cox(survival::Surv(y_sim, event_sim) ~ treat_sim), baseline = c("resample", "fixed"), n = NULL, analysis_time, max_entry, cens_adjust, cutpoints = list(), benchmarks = benchmark_spec(), min_n = 5L, workers = NULL, seed_base = 0L, rand_seed_offset = 1000000L, hr_true = NULL, k_treat = NULL, future_packages = c("forestsearch", "survival"), validate = TRUE, verbose = FALSE )run_subgroup_sims( dgm, subgroups, n_sims, fit = subgroup_cox(survival::Surv(y_sim, event_sim) ~ treat_sim), baseline = c("resample", "fixed"), n = NULL, analysis_time, max_entry, cens_adjust, cutpoints = list(), benchmarks = benchmark_spec(), min_n = 5L, workers = NULL, seed_base = 0L, rand_seed_offset = 1000000L, hr_true = NULL, k_treat = NULL, future_packages = c("forestsearch", "survival"), validate = TRUE, verbose = FALSE )
dgm |
A DGM from |
subgroups |
List of subgroup definitions, each a list with
character scalars |
n_sims |
Number of simulated trials. |
fit |
Per-subgroup analysis function |
baseline |
|
n |
Patients per trial. Required for |
analysis_time, max_entry, cens_adjust
|
Passed to
|
cutpoints |
Named list of scalars exposed as columns of each
simulated trial so |
benchmarks |
A |
min_n |
Subgroups with fewer rows are skipped for that trial
(row stays |
workers |
Parallel workers. |
seed_base, rand_seed_offset
|
Seed scheme; defaults ( |
hr_true, k_treat
|
Optional provenance values stored verbatim in
the result (the vignettes store |
future_packages |
Packages loaded on workers, default
|
validate |
Run |
verbose |
Print progress lines. |
Per-iteration seeding is explicit (trial ss uses
seed_base + ss for the DGM draw and
seed_base + ss + rand_seed_offset for benchmark membership), so
results are independent of the parallel schedule: any workers
setting, including sequential, gives identical matrices.
GLM dispatch: when dgm inherits "glm_dgm" (generate_glm_dgm()),
trials are drawn by simulate_from_glm_dgm() (n is required; the
survival-only analysis_time / max_entry / cens_adjust must not
be supplied; baseline = "fixed" uses the stored df_source panel), the default
fit becomes subgroup_glm() constructed from the DGM's own
outcome_type and effect_measure (binary DGMs also pass their
adverse_outcome; a continuous DGM resolves to the same fitter as
before), and the result additionally carries
the fitter's effect scale metadata with design = "glm". The
sim_hrs / sim_ubs matrices then hold that fitter's
(estimate, upper bound) pairs – the field names are retained for
structural compatibility, exactly as hr.threshold serves
generic-threshold duty for GLM outcomes in forestsearch().
An object of class "subgroup_sims": a named list with
design, n_sims, matrices sim_hrs / sim_ubs / sim_ns
(n_sims x subgroups, columns named by subgroup name),
subgroups, sim_config, cens_adjust, k_treat, hr_true,
created, r_version, and forestsearch_version. For a
"glm_dgm": design = "glm", cens_adjust = NULL, and an
additional effect field holding the fitter's scale metadata (see
subgroup_glm()); survival results are constructed exactly as
before, with no new fields.
summary.subgroup_sims(), benchmark_spec(),
subgroup_cox(), subgroup_glm(), validate_subgroups()
## Not run: sims <- run_subgroup_sims( dgm = dgm_uniform, subgroups = subgroups, n_sims = 1000, fit = subgroup_cox(survival::Surv(y_sim, event_sim) ~ treat_sim + survival::strata(grade)), baseline = "fixed", analysis_time = 84, max_entry = 24, cens_adjust = cal_uniform$cens_adjust, cutpoints = list(age_med = median(gbsg$age)), hr_true = 0.70, k_treat = k_treat_uniform ) saveRDS(sims, "results/extreme_sims_fixed_1000_payload.rds") ## End(Not run)## Not run: sims <- run_subgroup_sims( dgm = dgm_uniform, subgroups = subgroups, n_sims = 1000, fit = subgroup_cox(survival::Surv(y_sim, event_sim) ~ treat_sim + survival::strata(grade)), baseline = "fixed", analysis_time = 84, max_entry = 24, cens_adjust = cal_uniform$cens_adjust, cutpoints = list(age_med = median(gbsg$age)), hr_true = 0.70, k_treat = k_treat_uniform ) saveRDS(sims, "results/extreme_sims_fixed_1000_payload.rds") ## End(Not run)
Saves a forest plot to a file (PDF, PNG, etc.) with explicit dimensions.
save_forestplot(x, filename, width = 12, height = 10, dpi = 300, bg = "white")save_forestplot(x, filename, width = 12, height = 10, dpi = 300, bg = "white")
x |
An fs_forestplot object. |
filename |
Character. Output filename. Extension determines format. |
width |
Numeric. Plot width in inches. Default: 12. |
height |
Numeric. Plot height in inches. Default: 10. |
dpi |
Numeric. Resolution for raster formats. Default: 300. |
bg |
Character. Background color. Default: "white". |
Invisibly returns the filename.
## Not run: # Create plot with custom theme large_theme <- create_forest_theme(base_size = 14, row_padding = c(6, 4)) result <- plot_subgroup_results_forestplot( fs_results = list(fs.est = fs, fs_bc = fs_bc), df_analysis = df.analysis, outcome.name = "time", event.name = "status", treat.name = "treatment", theme = large_theme ) # Save to file save_forestplot(result, "forest_plot.pdf", width = 14, height = 12) ## End(Not run)## Not run: # Create plot with custom theme large_theme <- create_forest_theme(base_size = 14, row_padding = c(6, 4)) result <- plot_subgroup_results_forestplot( fs_results = list(fs.est = fs, fs_bc = fs_bc), df_analysis = df.analysis, outcome.name = "time", event.name = "status", treat.name = "treatment", theme = large_theme ) # Save to file save_forestplot(result, "forest_plot.pdf", width = 14, height = 12) ## End(Not run)
Writes a standardized .rds bundle containing the fitted
L_eff parameters and all supporting data. Called at the end of
calibration documents (e.g., calibration_binary_leff.qmd).
save_leff_calibration( C, alpha, n_min, outcome_type, cal_data = NULL, dgm_description = "", n_sims_per_N = NA_integer_, output_dir = NULL, extra = list() )save_leff_calibration( C, alpha, n_min, outcome_type, cal_data = NULL, dgm_description = "", n_sims_per_N = NA_integer_, output_dir = NULL, extra = list() )
C |
Numeric. Fitted scale parameter. |
alpha |
Numeric. Fitted power-law exponent. |
n_min |
Integer. Reference minimum subgroup size. |
outcome_type |
Character. One of "binary", "survival", "count", "continuous". |
cal_data |
Data frame with columns N, sim_fpr, P1, L_eff (the raw calibration points). |
dgm_description |
Character. Free-text description of the DGM used for calibration. |
n_sims_per_N |
Integer. Simulations per sample size. |
output_dir |
Character. Directory for .rds files. Required: an
error is raised when it is |
extra |
List. Any additional items to store. |
Invisible path to the saved file.
## Not run: save_leff_calibration( C = 0.220, alpha = 1.298, n_min = 60, outcome_type = "binary", cal_data = cal_df, dgm_description = "Binary DGM, 4 confounders (bm1, bm2, age, ecog)", n_sims_per_N = 5000, output_dir = tempdir() ) ## End(Not run)## Not run: save_leff_calibration( C = 0.220, alpha = 1.298, n_min = 60, outcome_type = "binary", cal_data = cal_df, dgm_description = "Binary DGM, 4 confounders (bm1, bm2, age, ecog)", n_sims_per_N = 5000, output_dir = tempdir() ) ## End(Not run)
Identifies the optimal subgroup according to the specified criterion
select_best_subgroup(values, sg.criterion, dmin.grf, n.max)select_best_subgroup(values, sg.criterion, dmin.grf, n.max)
values |
Data frame. Node metrics from policy trees |
sg.criterion |
Character. "mDiff" for maximum difference, "Nsg" for largest size |
dmin.grf |
Numeric. Minimum difference threshold |
n.max |
Integer. Maximum allowed subgroup size (total sample size) |
Data frame row with best subgroup or NULL if none found
vals <- data.frame(diff = c(8.5, 6.2, 3.1), Nsg = c(120, 95, 80)) select_best_subgroup(values = vals, sg.criterion = "mDiff", dmin.grf = 6, n.max = 500)vals <- data.frame(diff = c(8.5, 6.2, 3.1), Nsg = c(120, 95, 80)) select_best_subgroup(values = vals, sg.criterion = "mDiff", dmin.grf = 6, n.max = 500)
Analyzes how the interaction parameter k_inter affects hazard ratios in different populations (overall, harm subgroup, no-harm subgroup).
sensitivity_analysis_k_inter( k_inter_range = c(-5, 5), n_points = 21, plot = TRUE, ... )sensitivity_analysis_k_inter( k_inter_range = c(-5, 5), n_points = 21, plot = TRUE, ... )
k_inter_range |
Numeric vector of length 2 specifying the range of k_inter values to analyze. Default is c(-5, 5). |
n_points |
Integer number of points to evaluate within the range. Default is 21. |
plot |
Logical indicating whether to create visualization plots. Default is TRUE. |
... |
Additional arguments passed to |
This function evaluates the hazard ratios at evenly spaced points across the k_inter range. If plot = TRUE, it creates a 4-panel visualization showing:
Harm subgroup HR vs k_inter
All HRs (overall, harm, no-harm) vs k_inter
Ratio of HRs (harm/no-harm) showing effect modification
Table of key values
A data.frame of class "k_inter_sensitivity" with columns:
Numeric k_inter value
Numeric hazard ratio in harm subgroup
Numeric hazard ratio in no-harm subgroup
Numeric overall hazard ratio
Integer size of harm subgroup
## Not run: # Analyze sensitivity to k_inter sensitivity_results <- sensitivity_analysis_k_inter( k_inter_range = c(-2, 2), n_points = 11, data = survival::gbsg, continuous_vars = c("age", "er", "pgr"), factor_vars = c("meno", "grade"), outcome_var = "rfstime", event_var = "status", treatment_var = "hormon", subgroup_vars = c("er", "meno"), subgroup_cuts = list(er = 20, meno = 0), model = "alt", plot = TRUE ) # Results show relationship between k_inter and HRs print(sensitivity_results) ## End(Not run)## Not run: # Analyze sensitivity to k_inter sensitivity_results <- sensitivity_analysis_k_inter( k_inter_range = c(-2, 2), n_points = 11, data = survival::gbsg, continuous_vars = c("age", "er", "pgr"), factor_vars = c("meno", "grade"), outcome_var = "rfstime", event_var = "status", treatment_var = "hormon", subgroup_vars = c("er", "meno"), subgroup_cuts = list(er = 20, meno = 0), model = "alt", plot = TRUE ) # Results show relationship between k_inter and HRs print(sensitivity_results) ## End(Not run)
Creates a GBSG-based data generating mechanism that is fully compatible with
simulate_from_dgm. This is the replacement for
create_gbsg_dgm(): it accepts exactly the same arguments and produces
the same numeric output, but returns an object of class
"aft_dgm_flex" instead of "gbsg_dgm".
setup_gbsg_dgm( model = c("alt", "null"), k_treat = 1, k_inter = 1, k_z3 = 1, z1_quantile = 0.25, n_super = 5000L, cens_type = c("weibull", "uniform"), use_rand_params = FALSE, seed = 8316951L, verbose = FALSE, k_random_noise = 0L, noise_seed = 20260807L )setup_gbsg_dgm( model = c("alt", "null"), k_treat = 1, k_inter = 1, k_z3 = 1, z1_quantile = 0.25, n_super = 5000L, cens_type = c("weibull", "uniform"), use_rand_params = FALSE, seed = 8316951L, verbose = FALSE, k_random_noise = 0L, noise_seed = 20260807L )
model |
Character. Either "alt" for alternative hypothesis with heterogeneous treatment effects, or "null" for uniform treatment effect. Default: "alt" |
k_treat |
Numeric. Treatment effect multiplier applied to the treatment coefficient from the fitted AFT model. Values > 1 strengthen the treatment effect. Default: 1 |
k_inter |
Numeric. Interaction effect multiplier for the treatment-subgroup interaction (z1 * z3). Only used when model = "alt". Higher values create more heterogeneity between HR(H) and HR(Hc). Default: 1 |
k_z3 |
Numeric. Effect multiplier for the z3 (menopausal status) coefficient. Default: 1 |
z1_quantile |
Numeric. Quantile threshold for z1 (estrogen receptor). Observations with ER <= quantile are coded as z1 = 1. Default: 0.25 |
n_super |
Integer. Size of super-population for empirical HR estimation. Default: 5000 |
cens_type |
Character. Censoring distribution type: "weibull" or "uniform". Default: "weibull" |
use_rand_params |
Logical. If TRUE, modifies confounder coefficients using estimates from randomized subset (meno == 0). Default: FALSE |
seed |
Integer. Random seed for super-population generation. Default: 8316951 |
verbose |
Logical. Print diagnostic information. Default: FALSE |
k_random_noise |
Integer. Number of standard-normal noise columns
( |
noise_seed |
Integer. Seed for the population-level noise draw, used
only when |
Internally the function calls create_gbsg_dgm() and then:
Adds a df_super field with column names aligned to
simulate_from_dgm() conventions
(lin_pred_1, lin_pred_0, lin_pred_cens_1,
lin_pred_cens_0, flag_harm).
Adds a model_params$tau field (= model_params$sigma)
and a model_params$censoring sub-list.
Sets class to c("aft_dgm_flex", "gbsg_dgm", "list").
The original df_super_rand field is kept so that
compute_dgm_cde() and print.gbsg_dgm continue to work.
Noise columns requested via k_random_noise are inert
N(0,1) population attributes intended as candidate confounders; the
builder never puts them in the outcome model. They are drawn once onto
the super-population (both df_super_rand and df_super),
so simulated trials and evaluation frames inherit them by row resampling.
An object of class c("aft_dgm_flex", "gbsg_dgm", "list")
with all fields from create_gbsg_dgm() plus:
df_superSuper-population data frame with
simulate_from_dgm()-compatible column names.
model_params$tauCopy of model_params$sigma.
model_params$censoringSub-list with
type, mu, tau for the censoring model.
noise_namesCharacter vector of noise column names
(character(0) when k_random_noise = 0).
noise_scheme"population" when noise was drawn,
otherwise "none".
noise_seedThe seed used for the noise draw, or
NA_integer_ when none was drawn.
create_gbsg_dgm, simulate_from_dgm,
compute_dgm_cde
## Not run: dgm <- setup_gbsg_dgm(model = "alt", k_inter = 2, verbose = FALSE) dgm <- compute_dgm_cde(dgm) print(dgm) sim <- simulate_from_dgm(dgm, n = 400, seed = 1) ## End(Not run)## Not run: dgm <- setup_gbsg_dgm(model = "alt", k_inter = 2, verbose = FALSE) dgm <- compute_dgm_cde(dgm) print(dgm) sim <- simulate_from_dgm(dgm, n = 400, seed = 1) ## End(Not run)
Sets up parallel processing using the specified approach and number of workers.
setup_parallel_SGcons( parallel_args = list(plan = "multisession", workers = 4, show_message = TRUE) )setup_parallel_SGcons( parallel_args = list(plan = "multisession", workers = 4, show_message = TRUE) )
parallel_args |
List with |
None. Sets up parallel backend as side effect.
setup_parallel_SGcons(list(plan = "sequential", workers = 1, show_message = FALSE)) future::plan(future::sequential) # resetsetup_parallel_SGcons(list(plan = "sequential", workers = 1, show_message = FALSE)) future::plan(future::sequential) # reset
Returns the top subgroup(s) and recommended treatment flags.
sg_consistency_out( df, result_new, sg_focus, index.Z, names.Z, details = FALSE, plot.sg = FALSE, by.risk = 12, confs_labels, is_glm = FALSE, selection_rule = "neighborhood", effect_neighborhood = 0.1, effect_log_scale = FALSE )sg_consistency_out( df, result_new, sg_focus, index.Z, names.Z, details = FALSE, plot.sg = FALSE, by.risk = 12, confs_labels, is_glm = FALSE, selection_rule = "neighborhood", effect_neighborhood = 0.1, effect_log_scale = FALSE )
df |
Data.frame. Original analysis data. |
result_new |
Data.table. Sorted subgroup results. |
sg_focus |
Character. Sorting focus criterion. |
index.Z |
Matrix. Subgroup factor indicators. |
names.Z |
Character vector. Factor column names. |
details |
Logical. Print details. |
plot.sg |
Logical. Plot subgroup curves. |
by.risk |
Numeric. Risk interval for plotting. |
confs_labels |
Character vector. Human-readable labels. |
is_glm |
Logical. If |
selection_rule |
Character. Rule defining the candidate
inclusion set for |
effect_neighborhood |
Numeric in |
effect_log_scale |
Logical. If |
List with elements:
Sorted candidate table (top row = selected subgroup).
Data.table of non-dominated candidates on
(effect, N), both maximized. See
compute_pareto_frontier. May be NULL if
computation failed.
Factor-level cut names defining the selected subgroup.
Human-readable subgroup labels.
Per-subject treatment-recommendation flags.
Per-subject subgroup-membership indicator.
## Not run: # sg_consistency_out is called internally by forestsearch(). # See forestsearch() for the standard entry point. fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") fs$grp.consistency$out_sg ## End(Not run)## Not run: # sg_consistency_out is called internally by forestsearch(). # See forestsearch() for the standard entry point. fs <- forestsearch(gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") fs$grp.consistency$out_sg ## End(Not run)
Returns a summary table of subgroup estimates (HR, RMST, medians, etc.).
SG_tab_estimates( df, SG_flag, outcome.name = "tte", event.name = "event", treat.name = "treat", strata.name = NULL, hr_1a = NA, hr_0a = NA, potentialOutcome.name = NULL, sg1_name = NULL, sg0_name = NULL, draws = 0, details = FALSE, return_medians = TRUE, est.scale = "hr" )SG_tab_estimates( df, SG_flag, outcome.name = "tte", event.name = "event", treat.name = "treat", strata.name = NULL, hr_1a = NA, hr_0a = NA, potentialOutcome.name = NULL, sg1_name = NULL, sg0_name = NULL, draws = 0, details = FALSE, return_medians = TRUE, est.scale = "hr" )
df |
Data frame. |
SG_flag |
Character. Subgroup flag variable. |
outcome.name |
Character. Name of outcome variable. |
event.name |
Character. Name of event indicator variable. |
treat.name |
Character. Name of treatment variable. |
strata.name |
Character. Name of strata variable (optional). |
hr_1a |
Character. Adjusted HR for subgroup 1 (optional). |
hr_0a |
Character. Adjusted HR for subgroup 0 (optional). |
potentialOutcome.name |
Character. Name of potential outcome variable (optional). |
sg1_name |
Character. Name for subgroup 1. |
sg0_name |
Character. Name for subgroup 0. |
draws |
Integer. Number of draws for resampling (optional). |
details |
Logical. Print details. |
return_medians |
Logical. Use medians or RMST. |
est.scale |
Character. Effect scale ("hr" or "1/hr"). |
Data frame of subgroup summary estimates.
## Not run: library(survival) df <- data.frame( tte = gbsg$rfstime / 30.4375, event = gbsg$status, treat = gbsg$hormon, treat.recommend = as.integer(gbsg$er > 0) ) SG_tab_estimates(df, SG_flag = "ITT", outcome.name = "tte", event.name = "event", treat.name = "treat") ## End(Not run)## Not run: library(survival) df <- data.frame( tte = gbsg$rfstime / 30.4375, event = gbsg$status, treat = gbsg$hormon, treat.recommend = as.integer(gbsg$er > 0) ) SG_tab_estimates(df, SG_flag = "ITT", outcome.name = "tte", event.name = "event", treat.name = "treat") ## End(Not run)
Returns formatted summary tables for subgroups using the gt package, with search metadata and customizable decimal precision. Produces two tables: a treatment effect estimates table and an identified subgroups table, each with fully customizable titles and subtitles.
sg_tables( fs, which_df = "est", est_title = "Treatment Effect Estimates", est_caption = "Training data estimates", sg_title = "Identified Subgroups", sg_subtitle = NULL, potentialOutcome.name = NULL, hr_1a = NA, hr_0a = NA, ndecimals = 3, include_search_info = TRUE, subgroup_notation = NULL, font_size = 12 )sg_tables( fs, which_df = "est", est_title = "Treatment Effect Estimates", est_caption = "Training data estimates", sg_title = "Identified Subgroups", sg_subtitle = NULL, potentialOutcome.name = NULL, hr_1a = NA, hr_0a = NA, ndecimals = 3, include_search_info = TRUE, subgroup_notation = NULL, font_size = 12 )
fs |
ForestSearch results object, or a |
which_df |
Character. Which data frame to use ("est" or "testing"). |
est_title |
Character or NULL. Main title for the estimates table
(default: "Treatment Effect Estimates"). Rendered as bold markdown.
Set to NULL to suppress the title and display only |
est_caption |
Character. Subtitle for the estimates table (default: "Training data estimates"). |
sg_title |
Character or NULL. Main title for the identified subgroups
table (default: "Identified Subgroups"). Rendered as bold markdown.
Set to NULL to suppress the title and display only |
sg_subtitle |
Character or NULL. Subtitle for the identified subgroups
table. When NULL (default), an informative subtitle is auto-generated
from |
potentialOutcome.name |
Character. Name of potential outcome variable (optional). |
hr_1a |
Character. Adjusted HR for subgroup 1 (optional). |
hr_0a |
Character. Adjusted HR for subgroup 0 (optional). |
ndecimals |
Integer. Number of decimals for formatted numbers
(default: 3). Controls precision in both |
include_search_info |
Logical. Include search metadata table (default: TRUE). |
subgroup_notation |
Character or |
font_size |
Numeric. Font size in pixels for table text (default: 12). |
List with gt tables for estimates, subgroups, and optionally search info.
## Not run: library(survival) fs <- forestsearch( gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status", fs.splits = 50, use_lasso = FALSE, use_grf = FALSE ) tabs <- sg_tables(fs) tabs$tab_estimates ## End(Not run)## Not run: library(survival) fs <- forestsearch( gbsg, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status", fs.splits = 50, use_lasso = FALSE, use_grf = FALSE ) tabs <- sg_tables(fs) tabs$tab_estimates ## End(Not run)
Computes a reasonable knitr fig.height (in inches) for a
plot_sg_distribution() panel based on the number of
bars, keeping the visual quality consistent across scenarios with
very different numbers of distinct subgroup labels.
sgdist_fig_height( n_bars, floor_in = 4, ceil_in = 10, per_bar = 0.35, base_in = 3 )sgdist_fig_height( n_bars, floor_in = 4, ceil_in = 10, per_bar = 0.35, base_in = 3 )
n_bars |
Integer. Number of bars in the plot, typically
retrieved via |
floor_in |
Numeric. Minimum height in inches.
Default: |
ceil_in |
Numeric. Maximum height in inches.
Default: |
per_bar |
Numeric. Height contribution per bar in inches.
Default: |
base_in |
Numeric. Base height added to |
Numeric scalar, a height in inches in [floor_in,
ceil_in]. Suitable for the fig.height chunk option.
## Not run: p <- plot_sg_distribution(results, placeholder_on_empty = TRUE) h <- sgdist_fig_height(attr(p, "n_bars")) # Use `h` as the fig.height chunk option. ## End(Not run)## Not run: p <- plot_sg_distribution(results, placeholder_on_empty = TRUE) h <- sgdist_fig_height(attr(p, "n_bars")) # Use `h` as the fig.height chunk option. ## End(Not run)
Creates violin plots with embedded boxplots showing the distribution of hazard ratio estimates across simulations for different analysis populations. Supports symmetric trimming to handle extreme values that can distort the display when small subgroups produce very large HR estimates.
SGplot_estimates( df, label_training = "Training", label_testing = "Testing", label_itt = "ITT (stratified)", label_sg = "Testing (subgroup)", trim_fraction = NULL, ylim = NULL, show_summary = NULL, title = "Distribution of HR Estimates Across Simulations", subtitle = NULL )SGplot_estimates( df, label_training = "Training", label_testing = "Testing", label_itt = "ITT (stratified)", label_sg = "Testing (subgroup)", trim_fraction = NULL, ylim = NULL, show_summary = NULL, title = "Distribution of HR Estimates Across Simulations", subtitle = NULL )
df |
data.frame or data.table. Simulation results from
|
label_training |
Character. Label for training data estimates. Default: "Training" |
label_testing |
Character. Label for testing data estimates. Default: "Testing" |
label_itt |
Character. Label for ITT estimates. Default: "ITT (stratified)" |
label_sg |
Character. Label for subgroup estimates. Default: "Testing (subgroup)" |
trim_fraction |
Numeric or NULL. Fraction of observations to trim from each tail (e.g., 0.01 trims the lowest 1\ When non-NULL, trimmed means and SDs are computed for each group, extreme observations are flagged, and the y-axis is clipped to the trimmed data range. Set to NULL (default) for no trimming (backward compatible). |
ylim |
Numeric vector of length 2 or NULL. Explicit y-axis limits
as |
show_summary |
Logical. Annotate each violin with mean (SD) below
the x-axis labels. When trimming is active, displays trimmed
statistics. Default: TRUE when |
title |
Character. Plot title. Default: "Distribution of HR Estimates Across Simulations". |
subtitle |
Character or NULL. Plot subtitle. When trimming is active and subtitle is NULL, an auto-generated note indicating the trim fraction and number of flagged observations is shown. Default: NULL. |
List with components:
data.table formatted for plotting, with a
trimmed logical column when trimming is active
ggplot2 object
List of per-group trimming diagnostics (NULL when
no trimming). Each element contains: n_total, n_trimmed,
n_flagged, raw_mean, raw_sd, trimmed_mean,
trimmed_sd, lower_bound, upper_bound.
mrct_region_sims for generating simulation results,
summaryout_mrct for tabular summaries with trimming
## Not run: # Default (no trimming) — backward compatible plot_results <- SGplot_estimates( results_alt, label_training = "Non-Region A, ITT", label_itt = "Overall, ITT", label_testing = "Region A, ITT", label_sg = "Region A, identified subgroup" ) print(plot_results$plot_estimates) # With 1% symmetric trimming plot_results <- SGplot_estimates( results_alt, label_training = "Non-AP, ITT", label_itt = "Overall, ITT", label_testing = "AP, ITT", label_sg = "AP, identified subgroup", trim_fraction = 0.01 ) print(plot_results$plot_estimates) # Inspect trimming diagnostics print(plot_results$trim_info) # With explicit y-axis limits plot_results <- SGplot_estimates( results_alt, trim_fraction = 0.01, ylim = c(0.2, 3.0) ) ## End(Not run)## Not run: # Default (no trimming) — backward compatible plot_results <- SGplot_estimates( results_alt, label_training = "Non-Region A, ITT", label_itt = "Overall, ITT", label_testing = "Region A, ITT", label_sg = "Region A, identified subgroup" ) print(plot_results$plot_estimates) # With 1% symmetric trimming plot_results <- SGplot_estimates( results_alt, label_training = "Non-AP, ITT", label_itt = "Overall, ITT", label_testing = "AP, ITT", label_sg = "AP, identified subgroup", trim_fraction = 0.01 ) print(plot_results$plot_estimates) # Inspect trimming diagnostics print(plot_results$trim_info) # With explicit y-axis limits plot_results <- SGplot_estimates( results_alt, trim_fraction = 0.01, ylim = c(0.2, 3.0) ) ## End(Not run)
Region-consistency metrics on a simulated trial (ITT and optionally a subgroup)
sim_region_metrics(sim, region_col, subgroup_index = NULL, pi_preserve = 0.5)sim_region_metrics(sim, region_col, subgroup_index = NULL, pi_preserve = 0.5)
sim |
Data frame of one simulated trial with columns |
region_col |
Character name of the 0/1 region indicator column in
|
subgroup_index |
Optional logical vector of length |
pi_preserve |
Numeric threshold for the Method 1 consistency check:
the check passes when log(HR region) / log(HR overall) is at least
|
A list with numeric vectors overall, non_region and region
(each with elements hr, lo, hi, n, d; NA HR and limits when
fewer than 10 subjects or 5 events), consistency_ratio, logical
method1_pass, method2_pass and region_hr_below_1, and, when
subgroup_index is supplied, region_subgroup and
non_region_subgroup.
Generates simulated survival data from a previously created AFT data generating mechanism (DGM). Samples from the super population and generates survival times with specified censoring.
simulate_from_dgm( dgm, n = NULL, rand_ratio = 1, entry_var = NULL, max_entry = 24, analysis_time = 48, cens_adjust = 0, draw_treatment = TRUE, seed = NULL, strata_rand = NULL, hrz_crit = NULL, keep_rand = FALSE, time_eos = NULL, replace = TRUE, baseline = c("resample", "fixed") )simulate_from_dgm( dgm, n = NULL, rand_ratio = 1, entry_var = NULL, max_entry = 24, analysis_time = 48, cens_adjust = 0, draw_treatment = TRUE, seed = NULL, strata_rand = NULL, hrz_crit = NULL, keep_rand = FALSE, time_eos = NULL, replace = TRUE, baseline = c("resample", "fixed") )
dgm |
An object of class |
n |
Integer specifying the sample size. If |
rand_ratio |
Numeric randomisation ratio (treatment:control).
Default |
entry_var |
Character string naming an entry-time variable in the
super population. If |
max_entry |
Numeric maximum entry time for staggered entry simulation.
Only used when |
analysis_time |
Numeric calendar time of analysis. Follow-up is
|
cens_adjust |
Numeric log-scale adjustment to censoring distribution.
Positive values increase censoring times; negative values decrease them.
Default |
draw_treatment |
Logical. If |
seed |
Integer random seed. Default |
strata_rand |
Character string naming a column in the sampled data
for within-stratum balanced treatment allocation. If |
hrz_crit |
Numeric log-HR threshold. If supplied, a column
|
keep_rand |
Logical. If |
time_eos |
Numeric secondary administrative censoring cutoff
(end-of-study time on the DGM scale). Applied after |
replace |
Logical sampling scheme for drawing the |
baseline |
Character; how baseline covariates are obtained per
simulated trial. |
All time parameters (analysis_time, max_entry,
time_eos) must be expressed in the same units as
outcome_var supplied to generate_aft_dgm_flex(). A common
error is building the DGM on days (e.g. rfstime) and then passing
analysis_time in months, which causes follow-up windows far shorter
than the DGM event-time scale and produces universal administrative
censoring (event_sim = 0 for all subjects).
Verify with: exp(dgm$model_params$mu) — the implied median event
time should be plausible given your analysis_time.
When n = NULL the entire super population is used as-is, with no
staggered entry and no administrative censoring (follow_up = Inf).
Treatment assignments and linear predictors already stored in
dgm$df_super are retained unchanged.
cens_adjust shifts the log-scale location parameter of the
censoring distribution:
cens_adjust = log(2) doubles expected censoring times.
cens_adjust = log(0.5) halves expected censoring times.
baseline)Under baseline = "resample" each simulated trial is a fresh draw
of subjects, so between-trial variability reflects both covariate
resampling and outcome generation – the "what if this trial had enrolled
a different sample from the same population?" interpretation. Subgroup
sizes fluctuate across trials.
Under baseline = "fixed" the covariate matrix is the observed
trial data, identical in every simulated trial: subgroup definitions,
memberships, and sizes are exactly those of the source data. Between-trial
variability isolates the outcome-generation process (treatment
assignment, latent event times, censoring, entry). This is the
conditional-on-X (parametric-bootstrap-given-X) design. Note that entry
times are still drawn Uniform(0, max_entry) per trial unless
entry_var names a fixed column, and treatment is still
re-randomised per trial unless draw_treatment = FALSE.
A data.frame with columns:
idSubject identifier.
treatOriginal treatment from super population.
treat_simSimulated treatment assignment.
flag_harmSubgroup indicator (1 = all subgroup conditions met).
z_*Covariate values.
lin_pred_1, lin_pred_0
Counterfactual log-time linear predictors.
y_simObserved survival time (min(T, C)).
event_simEvent indicator (1 = event, 0 = censored).
t_trueLatent true survival time (pre-censoring).
c_timeEffective censoring time (post admin-censoring).
hrz_flag(Optional) Individual harm-zone indicator.
rand_order(Optional) Randomisation sequence index.
generate_aft_dgm_flex, check_censoring_dgm
## Not run: dgm <- setup_gbsg_dgm(model = "null", verbose = FALSE) sim_data <- simulate_from_dgm(dgm, n = 200, seed = 42) dim(sim_data) head(sim_data[, c("y_sim", "event_sim", "treat_sim")]) ## End(Not run)## Not run: dgm <- setup_gbsg_dgm(model = "null", verbose = FALSE) sim_data <- simulate_from_dgm(dgm, n = 200, seed = 42) dim(sim_data) head(sim_data[, c("y_sim", "event_sim", "treat_sim")]) ## End(Not run)
Generates simulated clinical trial data from a GBSG-based data generating mechanism.
simulate_from_gbsg_dgm( dgm, n = NULL, rand_ratio = 1, sim_id = 1, max_follow = Inf, muC_adj = 0, min_cens = NULL, max_cens = NULL, draw_treatment = TRUE )simulate_from_gbsg_dgm( dgm, n = NULL, rand_ratio = 1, sim_id = 1, max_follow = Inf, muC_adj = 0, min_cens = NULL, max_cens = NULL, draw_treatment = TRUE )
dgm |
A "gbsg_dgm" object from |
n |
Integer. Sample size. If NULL, uses full super-population. Default: NULL |
rand_ratio |
Numeric. Randomization ratio (treatment:control). Default: 1 (1:1 randomization) |
sim_id |
Integer. Simulation ID used for seed offset. Default: 1 |
max_follow |
Numeric. Administrative censoring time (months). Default: Inf (no administrative censoring) |
muC_adj |
Numeric. Adjustment to censoring distribution location parameter. Positive values increase censoring. Default: 0 |
min_cens |
Numeric. Minimum censoring time for uniform censoring. Required if cens_type = "uniform" |
max_cens |
Numeric. Maximum censoring time for uniform censoring. Required if cens_type = "uniform" |
draw_treatment |
Logical. If TRUE, randomly assigns treatment. If FALSE, samples from existing treatment arms. Default: TRUE |
Data frame with simulated trial data including:
Subject identifier
Observed follow-up time
Event indicator (1 = event, 0 = censored)
True event time (before censoring)
Treatment indicator
Harm subgroup indicator
Individual log hazard ratio (potential outcome)
Analysis factors
## Not run: dgm <- create_gbsg_dgm(model = "alt", k_inter = 2.0) sim_data <- simulate_from_gbsg_dgm(dgm, n = 500, sim_id = 1) # Check AHR in simulated data exp(mean(sim_data$loghr_po)) ## End(Not run)## Not run: dgm <- create_gbsg_dgm(model = "alt", k_inter = 2.0) sim_data <- simulate_from_gbsg_dgm(dgm, n = 500, sim_id = 1) # Check AHR in simulated data exp(mean(sim_data$loghr_po)) ## End(Not run)
Generates a simulated clinical trial dataset by sampling from the
super-population in a "glm_dgm" object. Assigns treatment
and generates outcomes from the individual-level potential outcomes
stored in the DGM.
simulate_from_glm_dgm( dgm, n = NULL, rand_ratio = 1, seed = NULL, draw_treatment = TRUE, replace = TRUE, baseline = c("resample", "fixed") )simulate_from_glm_dgm( dgm, n = NULL, rand_ratio = 1, seed = NULL, draw_treatment = TRUE, replace = TRUE, baseline = c("resample", "fixed") )
dgm |
An object of class |
n |
Integer. Sample size. If |
rand_ratio |
Numeric. Treatment:control randomisation ratio.
Default |
seed |
Integer. Random seed. Default |
draw_treatment |
Logical. If |
replace |
Logical sampling scheme for drawing the |
baseline |
\"resample\" (default; draw the trial panel from the super-population) or \"fixed\" (use dgm$df_source – the observed panel, every patient exactly once, observed order; requires a DGM built by a version that stores df_source; n must be NULL or nrow(df_source)). |
A data frame with columns:
idInteger subject identifier.
y_simSimulated outcome.
treat_simTreatment indicator (0/1).
flag_harmTrue subgroup membership indicator.
Plus all covariate columns from the super-population.
For binary outcomes, y_sim is a 0/1 Bernoulli draw from
the potential outcome probabilities (p0 or p1).
There is no event_sim column – for ForestSearch, set
event.name = "y_sim".
generate_glm_dgm,
run_simulation_analysis
## Not run: dgm <- generate_glm_dgm(...) df <- simulate_from_glm_dgm(dgm, n = 500, seed = 42) table(df$y_sim, df$treat_sim) ## End(Not run)## Not run: dgm <- generate_glm_dgm(...) df <- simulate_from_glm_dgm(dgm, n = 500, seed = 42) table(df$y_sim, df$treat_sim) ## End(Not run)
Sorts a data.table of subgroup results according to the specified focus.
For "hrMaxSG" and "hrMinSG", candidates are first
partitioned into a candidate inclusion set (the "band"), then
ranked within the band by sample size. The selection_rule
argument controls how the band is defined:
"neighborhood" – 1D effect-size band (default; legacy
behaviour): a candidate is in-band iff its effect is within
effect_neighborhood of the maximum.
"pareto" – 2D Pareto-non-dominated set in
(effect, N) space: a candidate is in-band iff no other candidate
has both a larger effect and a larger N.
"both" – intersection of the two: a candidate is
in-band iff it satisfies the neighborhood test and is
Pareto-non-dominated.
See Details.
sort_subgroups( result_new, sg_focus, selection_rule = "neighborhood", effect_neighborhood = 0.1, effect_log_scale = FALSE )sort_subgroups( result_new, sg_focus, selection_rule = "neighborhood", effect_neighborhood = 0.1, effect_log_scale = FALSE )
result_new |
A data.table of subgroup results with columns
|
sg_focus |
Character. Sorting focus. One of |
selection_rule |
Character. Rule defining the candidate
inclusion set for |
effect_neighborhood |
Numeric in |
effect_log_scale |
Logical. If |
Sort keys by sg_focus:
"hr"(-Pcons, -hr, K) – prefer high consistency
and large effect.
"maxSG"(-N, -Pcons, K) – prefer large
subgroups with high consistency.
"minSG"(N, -Pcons, K) – prefer small
subgroups with high consistency.
"hrMaxSG"(-in_band, -N, -Pcons, -hr, K) – among
candidates in the inclusion band defined by selection_rule,
prefer the largest sample size.
"hrMinSG"(-in_band, N, -Pcons, -hr, K) – among
candidates in the inclusion band defined by selection_rule,
prefer the smallest sample size.
"hrMaxSG" and "hrMinSG" use a lexicographic with
candidate band rule: effect size is primary, but a bounded set of
candidates near (or trading off against) the maximum is accepted to
optimize sample size.
Selection rules:
"neighborhood"1D -band on the effect
scale: in_band = (hr >= (1 - effect_neighborhood) * max(hr)).
Setting effect_neighborhood = 0 reduces this to a strict
max-effect filter. This is the legacy behaviour.
"pareto"2D Pareto-non-dominated set: a candidate is
in-band iff no other candidate has both hr_j >= hr_i and
N_j >= N_i with at least one strict inequality. Reuses
the same dominance computation as compute_pareto_frontier().
Removes the effect_neighborhood tuning parameter from the
selection criterion.
"both"Intersection: in-band iff in the
-band and Pareto-non-dominated. Strictest
of the three.
A sorted data.table. The top row is the selected subgroup
under sg_focus; remaining rows are diagnostic.
library(data.table) dt <- data.table(Pcons = c(0.92, 0.95, 0.88, 0.90), hr = c(2.5, 2.4, 2.3, 2.0), N = c(70, 100, 150, 200), K = c(1, 2, 1, 2)) # hrMaxSG with 10% neighborhood: HR 2.5, 2.4, 2.3 are in-band; # among those, N = 150 is largest -> top row is N=150, hr=2.3. sort_subgroups(dt, sg_focus = "hrMaxSG", effect_neighborhood = 0.10) # Same data with selection_rule = "pareto": the dominance set in # (hr, N) is used instead of the 1D effect band. sort_subgroups(dt, sg_focus = "hrMaxSG", selection_rule = "pareto")library(data.table) dt <- data.table(Pcons = c(0.92, 0.95, 0.88, 0.90), hr = c(2.5, 2.4, 2.3, 2.0), N = c(70, 100, 150, 200), K = c(1, 2, 1, 2)) # hrMaxSG with 10% neighborhood: HR 2.5, 2.4, 2.3 are in-band; # among those, N = 150 is largest -> top row is N=150, hr=2.3. sort_subgroups(dt, sg_focus = "hrMaxSG", effect_neighborhood = 0.10) # Same data with selection_rule = "pareto": the dominance set in # (hr, N) is used instead of the 1D effect band. sort_subgroups(dt, sg_focus = "hrMaxSG", selection_rule = "pareto")
Returns a function data -> c(HR, UB) fitting formula by
survival::coxph() and extracting the first row of
summary(fit)$conf.int: the hazard-ratio point estimate
("exp(coef)") and the upper 95% bound ("upper .95"). Estimation
failure or an empty coefficient table returns c(NA, NA). This
reproduces the vignettes' cox_sR() exactly when called with
survival::Surv(y_sim, event_sim) ~ treat_sim + survival::strata(grade).
subgroup_cox(formula, lean = TRUE)subgroup_cox(formula, lean = TRUE)
formula |
A model formula for |
lean |
Re-home the formula environment as described above
(default |
Custom fitters may be passed to run_subgroup_sims() directly: any
function of a data frame returning a length-2 numeric
(estimate, upper bound) is accepted. Fitters run on parallel
workers, so they should reference non-base functions by
pkg::fun or rely on packages listed in future_packages.
A function of one argument (data) returning
c(hr, ub); carries the formula in attr(, "formula").
lean)A formula captures the environment where it was created. In a script
or a knitr chunk that is the global environment, and future's
globals machinery gives formulas special treatment: a fit whose
formula points at a populated environment imposes a multi-second
per-future cost on parallel runs (measured ~2-4 s x ~116 futures in
the Phase 2 timing diagnostic), independent of the serialized size.
With lean = TRUE (default) the formula is re-homed into a fresh,
empty environment parented on the survival namespace, cutting the
costly chain while keeping survival::Surv(), survival::strata(),
and base/stats symbols resolvable. Consequence: the formula must
reference only data columns and functions reachable from the
survival namespace (use pkg:: prefixes for anything else). Set
lean = FALSE to retain the calling environment – restoring
arbitrary-symbol resolution at that parallel cost. Independently of
lean, the returned closure is stripped of source references and
given a minimal environment holding only the formula (keep-source
installs otherwise attach ~250 KB of package-source baggage to every
worker shipment).
fit <- subgroup_cox(survival::Surv(y_sim, event_sim) ~ treat_sim)fit <- subgroup_cox(survival::Surv(y_sim, event_sim) ~ treat_sim)
Returns a function data -> c(estimate, UB) for non-survival outcomes
by delegating estimation to the package's native effect-estimator
layer: the closure from make_effect_estimator() (the same code the
forestsearch() search, bootstrap, and consistency loops use),
including the bit-identical unadjusted fast path
(.make_lm_estimator_fast()) under the same routing predicate
forestsearch() applies. The estimator's internal-scale
estimate/se are mapped to the wrapper contract as
upper = estimate + qnorm(1 - (1 - level)/2) * se, back-transformed
(exp()) for ratio measures – the package's own display convention
– so ratio-measure matrices land on the natural scale exactly as
subgroup_cox()'s exp(coef) / upper .95 do. Estimation failure,
non-convergence, NA estimate/SE (e.g. a single-arm subgroup, where
the treatment coefficient is aliased), or a non-finite result (e.g. a
zero-residual-df fit) returns c(NA, NA).
subgroup_glm( outcome.name = "y_sim", treat.name = "treat_sim", outcome_type = "continuous", effect_measure = NULL, adverse_outcome = TRUE, robust_se = TRUE, offset.name = NULL, adjust_covariates = NULL, level = 0.95, lean = TRUE )subgroup_glm( outcome.name = "y_sim", treat.name = "treat_sim", outcome_type = "continuous", effect_measure = NULL, adverse_outcome = TRUE, robust_se = TRUE, offset.name = NULL, adjust_covariates = NULL, level = 0.95, lean = TRUE )
outcome.name, treat.name
|
Column names of the outcome and the
0/1 treatment indicator; defaults match |
outcome_type |
|
effect_measure |
|
adverse_outcome |
Higher outcome = harm ( |
robust_se, offset.name, adjust_covariates
|
Passed to
|
level |
Confidence level for the upper bound (default |
lean |
Re-home the provenance formula's environment (default
|
A function of one argument (data) returning
c(estimate, ub); carries the provenance formula in
attr(, "formula") and scale metadata in attr(, "effect") – a
list with measure, outcome_type, log_scale, null_value,
adverse_outcome, est_label, ub_label, est_thresholds,
ub_thresholds, level – which run_subgroup_sims() stamps onto
GLM results for scale-aware summaries and plots. A custom fitter
can opt into the same behavior by attaching an "effect" attribute
of this shape.
Estimates follow the estimator layer's harm-positive normalization:
with adverse_outcome = FALSE (higher outcome = better) the outcome
is negated internally so that estimate > 0 consistently indicates
treatment harm, aligning with the survival convention where HR > 1 =
harm. Consequently the null value is always 0 (identity measures) or
1 (ratio measures), whatever the raw outcome's direction.
Phase 4.5 validates outcome_type = "continuous" (MD),
outcome_type = "binary" ("OR" – the binary default – and
"RD"), and outcome_type = "count" ("IRR" – the count default
– and "IRD"; offset.name is required, see below). Binary
"RR" (estimator-supported; wrapper parity pending) still stops
with an informative error; the estimator layer
(make_effect_estimator()) already supports it and the gate lifts
with its parity phase – no interface change.
lean)The returned closure is stripped of source references and given a
minimal environment (the estimator closure and two scalars), and the
estimator closure itself is rebuilt onto a fresh environment holding
only its forced argument values (parented on the package namespace,
which serializes by reference) with its own source references
stripped. This severs shared srcfile environments (keep-source
installs) and the constructor's frame chain from the serialized
graph, so the fitter ships to parallel workers in the low
single-digit KB regardless of session state. The provenance formula
attached in attr(, "formula") (built from the column names, e.g.
y_sim ~ treat_sim) is re-homed into an empty stats-parented
environment when lean = TRUE, keeping the parallel-shipment guard
in run_subgroup_sims() meaningful.
subgroup_cox(), run_subgroup_sims(),
make_effect_estimator(), generate_glm_dgm()
## Not run: fit <- subgroup_glm() # y_sim ~ treat_sim, MD fit(simulate_from_glm_dgm(dgm, n = 500, seed = 1)) # Binary (Phase 4.4): OR by default, RD available fit_or <- subgroup_glm(outcome_type = "binary") fit_rd <- subgroup_glm(outcome_type = "binary", effect_measure = "RD") # Count (Phase 4.5): IRR by default, IRD available; offset required fit_irr <- subgroup_glm(outcome_type = "count", offset.name = "t_exp") fit_ird <- subgroup_glm(outcome_type = "count", offset.name = "t_exp", effect_measure = "IRD") ## End(Not run)## Not run: fit <- subgroup_glm() # y_sim ~ treat_sim, MD fit(simulate_from_glm_dgm(dgm, n = 500, seed = 1)) # Binary (Phase 4.4): OR by default, RD available fit_or <- subgroup_glm(outcome_type = "binary") fit_rd <- subgroup_glm(outcome_type = "binary", effect_measure = "RD") # Count (Phase 4.5): IRR by default, IRD available; offset required fit_irr <- subgroup_glm(outcome_type = "count", offset.name = "t_exp") fit_ird <- subgroup_glm(outcome_type = "count", offset.name = "t_exp", effect_measure = "IRD") ## End(Not run)
Evaluates candidate subgroups using split-sample consistency validation. For each candidate, repeatedly splits the data and checks whether the treatment effect direction is consistent across splits.
subgroup.consistency( df, hr.subgroups, hr.threshold = 1, hr.consistency = 1, pconsistency.threshold = 0.9, m1.threshold = Inf, n.splits = 100, details = FALSE, by.risk = 12, plot.sg = FALSE, maxk = 7, Lsg, confs_labels, sg_focus = "hr", selection_rule = "neighborhood", effect_neighborhood = 0.1, stop_Kgroups = 200, stop_threshold = NULL, show_candidate_summary = FALSE, max_print = 10, pconsistency.digits = 2, seed = 8316951, checking = FALSE, use_twostage = FALSE, twostage_args = list(), parallel_args = list(), estimator_fn = NULL, consistency_threshold = NULL, effect_label = "HR", effect_log_scale = FALSE, adjust_covariates = NULL, consistency_method = c("resample", "split"), glm_resample_spec = NULL )subgroup.consistency( df, hr.subgroups, hr.threshold = 1, hr.consistency = 1, pconsistency.threshold = 0.9, m1.threshold = Inf, n.splits = 100, details = FALSE, by.risk = 12, plot.sg = FALSE, maxk = 7, Lsg, confs_labels, sg_focus = "hr", selection_rule = "neighborhood", effect_neighborhood = 0.1, stop_Kgroups = 200, stop_threshold = NULL, show_candidate_summary = FALSE, max_print = 10, pconsistency.digits = 2, seed = 8316951, checking = FALSE, use_twostage = FALSE, twostage_args = list(), parallel_args = list(), estimator_fn = NULL, consistency_threshold = NULL, effect_label = "HR", effect_log_scale = FALSE, adjust_covariates = NULL, consistency_method = c("resample", "split"), glm_resample_spec = NULL )
df |
Data frame containing the analysis dataset. Must include columns for outcome (Y), event indicator (Event), and treatment (Treat). |
hr.subgroups |
Data.table of candidate subgroups from subgroup search, containing columns: HR, n, E, K, d0, d1, m0, m1, grp, and factor indicators. |
hr.threshold |
Numeric. |
hr.consistency |
Numeric. |
pconsistency.threshold |
Numeric. |
m1.threshold |
Numeric. Maximum m1 threshold for filtering. Default: Inf |
n.splits |
Integer. Number of splits for consistency evaluation. Default: 100 |
details |
Logical. Print progress details. Default: FALSE |
by.risk |
Numeric. Risk interval for KM plots. Default: 12 |
plot.sg |
Logical. Generate subgroup plots. Default: FALSE |
maxk |
Integer. Maximum number of factors in subgroup. Default: 7 |
Lsg |
List of subgroup parameters. |
confs_labels |
Character vector mapping factor names to labels. |
sg_focus |
Character. Subgroup selection criterion. One of:
Default: |
selection_rule |
Character. Rule defining the candidate
inclusion set for |
effect_neighborhood |
Numeric in |
stop_Kgroups |
Integer. Maximum number of candidates to evaluate.
When the ranked pool exceeds this cap it is truncated and a warning naming
both counts and the |
stop_threshold |
Numeric in Note: Values > 1.0 are not permitted. To disable early
stopping, use Interaction with
For parallel execution, early stopping is checked after each batch
completes, so some additional candidates beyond the first meeting the
threshold may be evaluated. Use a smaller |
show_candidate_summary |
Logical. If |
max_print |
Integer. Maximum number of candidate rows printed in
each |
pconsistency.digits |
Integer. Number of decimal places to which the
consistency proportion is rounded before it is compared with
|
seed |
Integer. Random seed for reproducible consistency splits. Default: 8316951. Set to NULL for non-reproducible random splits. The seed is used both for sequential execution (via set.seed()) and parallel execution (via future.seed). |
checking |
Logical. Enable additional validation checks. Default: FALSE |
use_twostage |
Logical. Use two-stage adaptive algorithm. Default: FALSE |
twostage_args |
List. Parameters for two-stage algorithm:
|
parallel_args |
List. Parallel processing configuration:
|
estimator_fn |
Closure or |
consistency_threshold |
Numeric or |
effect_label |
Character. Column label for the effect measure in
diagnostic output (e.g., candidate subgroup table). Default |
effect_log_scale |
Logical. If |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
consistency_method |
Character. |
glm_resample_spec |
List or |
A list containing:
Selected subgroup results. When non-NULL,
contains result (sorted candidate table; top row is the
selected subgroup) and pareto_frontier (data.table of
non-dominated candidates on (effect, N), both maximized – a
post-hoc diagnostic, not used for selection).
Selection criterion used
Data frame with treatment recommendations
Subgroup definition labels – character
vector of factor-level cut names (e.g., c("z1.1", "z2.1")),
of length equal to the number of cuts defining the subgroup.
NULL if no subgroup was identified.
Per-subject subgroup-membership
indicator – integer vector of length nrow(df) with
1 if subject is in the identified subgroup and
0 otherwise. NULL if no subgroup was identified.
Not a character vector of cut expressions; see the
Field naming collision section below.
"twostage" or "fixed"
Number of candidates actually evaluated
Total candidates available
Number meeting consistency threshold
Logical indicating if early stop occurred
Index of candidate triggering early stop
Threshold used for early stopping
Random seed used for reproducibility (NULL if not set)
The field name sg.harm.id has different semantics on
this object versus on the result objects returned by
grf.subg.harm.glm and grf.subg.harm.survival:
| Object | sg.harm.id contains |
Length / type |
subgroup.consistency() result (this function) |
per-subject 0/1 membership indicator | integer, length nrow(df) |
grf.subg.harm.glm() result |
character vector of cut expressions | character, length = depth of selected tree |
grf.subg.harm.survival() result |
character vector of cut expressions | character, length = depth of selected tree |
Practical consequence. Do not paste sg.harm.id with
paste(..., collapse = " & ") to print "the identified subgroup"
without first checking object class. For subgroup labels from a
forestsearch or subgroup.consistency() result,
use sg.harm (character vector of cut names) – the FS main
result exposes sg.harm at the top level.
This naming collision is a documented CRAN-stable API for v0.1.x and v0.2.x; it is expected to be resolved via a deprecation cycle in a future minor release.
## Not run: # Standard evaluation result <- subgroup.consistency( df = trial_data, hr.subgroups = candidates, sg_focus = "hr", n.splits = 400, parallel_args = list(plan = "multisession", workers = 6) ) # Show top 10 candidates before evaluation result <- subgroup.consistency( df = trial_data, hr.subgroups = candidates, sg_focus = "hr", show_candidate_summary = TRUE, # Post-consistency summary n.splits = 400 ) # With early stopping and custom batch size result <- subgroup.consistency( df = trial_data, hr.subgroups = candidates, sg_focus = "hr", stop_threshold = 0.95, show_candidate_summary = TRUE, parallel_args = list( plan = "multisession", workers = 6, batch_size = 2 # Check early stopping after every 2 candidates ) ) ## End(Not run)## Not run: # Standard evaluation result <- subgroup.consistency( df = trial_data, hr.subgroups = candidates, sg_focus = "hr", n.splits = 400, parallel_args = list(plan = "multisession", workers = 6) ) # Show top 10 candidates before evaluation result <- subgroup.consistency( df = trial_data, hr.subgroups = candidates, sg_focus = "hr", show_candidate_summary = TRUE, # Post-consistency summary n.splits = 400 ) # With early stopping and custom batch size result <- subgroup.consistency( df = trial_data, hr.subgroups = candidates, sg_focus = "hr", stop_threshold = 0.95, show_candidate_summary = TRUE, parallel_args = list( plan = "multisession", workers = 6, batch_size = 2 # Check early stopping after every 2 candidates ) ) ## End(Not run)
Searches for subgroups with treatment effect heterogeneity using combinations of candidate factors. Evaluates subgroups for minimum prevalence, event counts, and hazard ratio threshold. Parallelizes the main search loop.
subgroup.search( Y, Event, Treat, ID = NULL, Z, n.min = 30, d0.min = 15, d1.min = 15, hr.threshold = 1, max.minutes = 30, minp = 0.05, rmin = 5, details = FALSE, maxk = 2, parallel_workers = parallel::detectCores(), estimator_fn = NULL, df_analysis = NULL, effect_threshold = NULL, effect_measure = NULL, outcome_type = NULL, adjust_covariates = NULL, disable_effect_floor = FALSE )subgroup.search( Y, Event, Treat, ID = NULL, Z, n.min = 30, d0.min = 15, d1.min = 15, hr.threshold = 1, max.minutes = 30, minp = 0.05, rmin = 5, details = FALSE, maxk = 2, parallel_workers = parallel::detectCores(), estimator_fn = NULL, df_analysis = NULL, effect_threshold = NULL, effect_measure = NULL, outcome_type = NULL, adjust_covariates = NULL, disable_effect_floor = FALSE )
Y |
Numeric vector of outcome (e.g., time-to-event). |
Event |
Numeric vector of event indicators (0/1). |
Treat |
Numeric vector of treatment group indicators (0/1). |
ID |
Optional vector of subject IDs. |
Z |
Matrix or data frame of candidate subgroup factors (binary indicators). |
n.min |
Integer. Minimum subgroup size. |
d0.min |
Integer. Minimum per-arm filter. For survival and binary
outcomes: minimum number of events in the control arm within the
candidate subgroup. Ignored for continuous outcomes (only
|
d1.min |
Integer. Same as |
hr.threshold |
Numeric. |
max.minutes |
Deprecated (0.4.0.9000): accepted and
ignored. It was documented as a wall-clock budget for the
combination search but was never enforced in the parallel search
path. Passing it emits one warning. Search scope is governed by
|
minp |
Numeric. Minimum prevalence rate for each factor. |
rmin |
Integer. Minimum required reduction in sample size when adding a factor. |
details |
Logical. Print details during execution. |
maxk |
Integer. Maximum number of factors in a subgroup. |
parallel_workers |
Integer. Number of parallel workers (default: all available cores). |
estimator_fn |
Closure or |
df_analysis |
Data frame or |
effect_threshold |
Numeric or |
effect_measure |
Character or |
outcome_type |
Character or |
adjust_covariates |
This option is implemented but has not been
tested against the manuscript analyses or the simulation studies of
León and Anderson (2026a, 2026b), which use the unadjusted
treatment-only fits.
Character vector or |
disable_effect_floor |
Logical. When |
List with found subgroups, maximum HR, search time, configuration info, and filtering statistics.
## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") Z <- df[, c("age", "meno", "size", "grade3", "nodes", "pgr", "er")] res <- subgroup.search(Y = df$rfstime, Event = df$status, Treat = df$hormon, Z = Z, hr.threshold = 1.25) ## End(Not run)## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") Z <- df[, c("age", "meno", "size", "grade3", "nodes", "pgr", "er")] res <- subgroup.search(Y = df$rfstime, Event = df$status, Treat = df$hormon, Z = Z, hr.threshold = 1.25) ## End(Not run)
Recommends (c1, c2) threshold pairs for ForestSearch based on
the corrected null approximation. Uses the per-subgroup detection
probability and an multiplicity correction to
find the smallest c1 (screening threshold) such that the
corrected FPR is at most fpr_target.
suggest_thresholds( d_eff, N, fpr_target = 0.1, n_min = 60, L_eff_C = 1, L_eff_alpha = 0, c1_range = c(1.05, 2.5), c1_step = 0.05, c2_candidates = seq(0.9, 2, by = 0.05), effect_scale = c("ratio", "difference") )suggest_thresholds( d_eff, N, fpr_target = 0.1, n_min = 60, L_eff_C = 1, L_eff_alpha = 0, c1_range = c(1.05, 2.5), c1_step = 0.05, c2_candidates = seq(0.9, 2, by = 0.05), effect_scale = c("ratio", "difference") )
d_eff |
Numeric. Effective information in the expected harm
subgroup. Use |
N |
Integer. Total sample size. |
fpr_target |
Numeric. Maximum acceptable procedure-level FPR. Default: 0.10. |
n_min |
Integer. Minimum subgroup size. Default: 60. |
L_eff_C |
Numeric. Calibration constant for
|
L_eff_alpha |
Numeric. Calibration exponent. Default: 0.0 (no N-dependence; see Details). |
c1_range |
Numeric vector of length 2. Search range for c1.
Default: |
c1_step |
Numeric. Grid step for c1. Default: 0.05. |
c2_candidates |
Numeric vector. Candidate c2 values to evaluate
for each c1. Default: |
effect_scale |
Character. |
The corrected FPR is:
where = compute_detection_probability_glm(1.0, d_eff, c1, c2)
and .
Default L_eff values (C=1, alpha=0) assume ,
which gives the per-subgroup approximation with no multiplicity
correction. This is appropriate when:
No calibration data is available
The analysis has few confounders and small N
For more accurate suggestions, calibrate from
an H0 simulation using calibrate_L_eff or
run_null_calibration.
Reference calibrations (from the forestsearch GLM extension analysis documents):
C = 0.220, alpha = 1.298 (binary_threshold_calibration.qmd, 5000 sims)
C = 0.029, alpha = 0.882 (cox_vs_glm_approximation.qmd, 500 sims per N)
A data.frame with columns:
Screening threshold.
Minimum consistency threshold achieving the target.
Corrected FPR at this (c1, min_c2).
Per-subgroup detection probability.
Effective number of candidates at this N.
Sorted by c1 ascending. The first row is the smallest c1 that achieves the target FPR.
compute_detection_probability_glm,
calibrate_L_eff,
predict_fpr_corrected
# Survival: paper defaults should be recovered d <- d_eff_survival(n_sg = 60, prop_cens = 0.45) suggest_thresholds(d_eff = d, N = 700) # Binary: needs higher thresholds (shown, not run -- about 10 s) # d <- d_eff_binary(n_sg = 150, p_event = 0.30) # suggest_thresholds(d_eff = d, N = 500, # L_eff_C = 0.220, L_eff_alpha = 1.298) # Use calibration object from run_null_calibration() # cal <- run_null_calibration(...) # suggest_thresholds(d_eff = d, N = 700, # L_eff_C = cal$C, L_eff_alpha = cal$alpha)# Survival: paper defaults should be recovered d <- d_eff_survival(n_sg = 60, prop_cens = 0.45) suggest_thresholds(d_eff = d, N = 700) # Binary: needs higher thresholds (shown, not run -- about 10 s) # d <- d_eff_binary(n_sg = 150, p_event = 0.30) # suggest_thresholds(d_eff = d, N = 500, # L_eff_C = 0.220, L_eff_alpha = 1.298) # Use calibration object from run_null_calibration() # cal <- run_null_calibration(...) # suggest_thresholds(d_eff = d, N = 700, # L_eff_C = cal$C, L_eff_alpha = cal$alpha)
Provides summary statistics for event counts across bootstrap iterations, helping assess the reliability of HR estimates when events are sparse.
summarize_bootstrap_events(boot_results, threshold = 5)summarize_bootstrap_events(boot_results, threshold = 5)
boot_results |
List. Output from forestsearch_bootstrap_dofuture() |
threshold |
Integer. Minimum event threshold for flagging low counts (default: 5) |
This function summarizes event counts in four scenarios:
ORIGINAL subgroup H evaluated on BOOTSTRAP samples
ORIGINAL subgroup Hc evaluated on BOOTSTRAP samples
NEW subgroup H* (found in bootstrap) evaluated on ORIGINAL data
NEW subgroup Hc* (found in bootstrap) evaluated on ORIGINAL data
Low event counts (below threshold) can lead to unstable HR estimates. This summary helps identify potential issues with sparse events.
Invisibly returns a list with summary statistics:
The event threshold used
Total number of bootstrap iterations
Number of iterations that found a new subgroup
List with low event counts for original H on bootstrap samples
List with low event counts for original Hc on bootstrap samples
List with low event counts for new H* on original data
List with low event counts for new Hc* on original data
## Not run: # After running bootstrap analysis summarize_bootstrap_events(fs_bc, threshold = 10) ## End(Not run)## Not run: # After running bootstrap analysis summarize_bootstrap_events(fs_bc, threshold = 10) ## End(Not run)
Creates comprehensive output including formatted table with subgroup footnote, diagnostic plots, bootstrap quality metrics, and detailed timing analysis.
summarize_bootstrap_results( sgharm, boot_results, create_plots = FALSE, est.scale = "hr", digits = 2 )summarize_bootstrap_results( sgharm, boot_results, create_plots = FALSE, est.scale = "hr", digits = 2 )
sgharm |
The selected subgroup object from forestsearch results. Can be:
|
boot_results |
List. Output from forestsearch_bootstrap_dofuture() |
create_plots |
Logical. Generate diagnostic plots (default: FALSE) |
est.scale |
Character. "hr" or "1/hr" for effect scale |
digits |
Integer or NULL. Decimal places for display formatting of
numeric columns in the rendered table (per-arm summaries, Diff,
effect estimate CI, bias-corrected CI). Default 2. When set, the
formatted strings in |
The table output includes a footnote displaying the identified subgroup
definition, analogous to the tab_estimates table from sg_tables.
This is achieved by extracting the subgroup definition from sgharm and
passing it to format_bootstrap_table.
List with components:
gt table with treatment effects and subgroup footnote
List of bootstrap quality metrics
gt table of diagnostics
List of ggplot2 diagnostic plots (if create_plots=TRUE)
List of timing analysis (if timing data available)
List from summarize_bootstrap_subgroups()
format_bootstrap_table for table creation
sg_tables for analogous main analysis tables
summarize_bootstrap_subgroups for subgroup stability analysis
## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") fs <- forestsearch(df, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") fs_bc <- forestsearch_bootstrap_dofuture(fs, nb_boots = 100) summaries <- summarize_bootstrap_results(fs$sg.harm, fs_bc) summaries$table ## End(Not run)## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") fs <- forestsearch(df, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "rfstime", treat.name = "hormon", event.name = "status") fs_bc <- forestsearch_bootstrap_dofuture(fs, nb_boots = 100) summaries <- summarize_bootstrap_results(fs$sg.harm, fs_bc) summaries$table ## End(Not run)
Comprehensive tabulation of bootstrap subgroup identification results
including basic statistics, factor frequencies, consistency
distributions, GRF-cut frequencies, and agreement with the original
analysis subgroup. Returns raw data.table tabulations
suitable for custom post-processing, export, or plotting; for the
full formatted pipeline (gt tables, diagnostics, plots) call
summarize_bootstrap_results instead.
summarize_bootstrap_subgroups(results, nb_boots, original_sg = NULL, maxk = 2)summarize_bootstrap_subgroups(results, nb_boots, original_sg = NULL, maxk = 2)
results |
Data.table or data.frame. Bootstrap results with subgroup characteristics including columns like Pcons, hr_sg, N_sg, K_sg, and M.1-M.k |
nb_boots |
Integer. Total number of bootstrap iterations |
original_sg |
Character vector. Original subgroup definition from main analysis (e.g., c("{age>=50}", "{nodes>=3}") for a 2-factor subgroup) |
maxk |
Integer. Maximum number of factors allowed in subgroup definition |
List with summary components:
Data.table of summary statistics
Data.table of Pcons distribution by bins
Data.table of subgroup size distribution
Data.table of factor frequencies by position
Data.table of subgroup definition agreement counts
Data.table of base factor presence counts
Data.table of specific factor definitions
Data.table of GRF policy-tree cut frequencies
across all bootstrap iterations (not just successful ones).
Columns: Rank, grf_cut, N,
Percent. NULL when results lacks the
grf_cuts_b column (pre-Phase-C objects). Unlike
subgroup-related tables, this tabulation is NOT restricted
to iterations that identified a subgroup – it spans all
bootstraps so that the analyst can distinguish "GRF produced
no cut" from "GRF produced a cut that consistency rejected".
Data.table comparing to original analysis subgroup
Integer. Number of successful iterations
Numeric. Percentage of successful iterations
summarize_bootstrap_results for the full
formatted summary pipeline (gt tables plus diagnostics and plots).
forestsearch_bootstrap_dofuture to run the bootstrap
analysis that produces the results object consumed here.
## Not run: # Run bootstrap analysis boot_out <- forestsearch_bootstrap_dofuture(fs.est, nb_boots = 500) # Raw tabulations for custom post-processing subg <- summarize_bootstrap_subgroups( results = boot_out$results, nb_boots = 500, original_sg = fs.est$sg.harm, maxk = 2L ) # Inspect the GRF cut frequencies across all bootstraps subg$grf_cut_freq # Export factor frequencies to CSV for an external report data.table::fwrite(subg$factor_freq, "factor_freq.csv") ## End(Not run)## Not run: # Run bootstrap analysis boot_out <- forestsearch_bootstrap_dofuture(fs.est, nb_boots = 500) # Raw tabulations for custom post-processing subg <- summarize_bootstrap_subgroups( results = boot_out$results, nb_boots = 500, original_sg = fs.est$sg.harm, maxk = 2L ) # Inspect the GRF cut frequencies across all bootstraps subg$grf_cut_freq # Export factor frequencies to CSV for an external report data.table::fwrite(subg$factor_freq, "factor_freq.csv") ## End(Not run)
Post-hoc diagnostic summary of forestsearch_tenfold output,
aggregating the sim x fold grid captured in fold_summary into
publication-ready gt tables. Parallel to
summarize_bootstrap_results but focused on cross-validation
stability and GRF candidate-discovery behaviour.
summarize_cv_results( cv_output, original_sg = NULL, original_grf_cuts = NULL, create_plots = FALSE, top_n = 15L )summarize_cv_results( cv_output, original_sg = NULL, original_grf_cuts = NULL, create_plots = FALSE, top_n = 15L )
cv_output |
List of class |
original_sg |
Character vector or |
original_grf_cuts |
Character vector or |
create_plots |
Logical. If |
top_n |
Integer. Maximum number of detail rows to retain in
frequency tables (default: |
An object of class "fs_cv_summary", a list with
components:
identification_summary, grf_cut_summary,
cut_vs_subgroup_xtab, no_subgroup_decomposition,
pconsistency_distribution, fold_numeric_summary,
original_agreement, metrics_table
gt
tables (or data.frame fallback if gt is
unavailable). Some may be NULL depending on input
columns and optional arguments.
plotsNULL or a list of ggplot objects.
dataList of raw data.frame tabulations
underlying the tables (identification, grf_cuts,
cut_vs_subgroup, no_subgroup, pconsistency,
fold_numeric_summary, original_agreement).
n_sims, n_folds, total_pairs
Grid dimensions.
has_grf_cuts, has_pconsistency
Logical.
Whether the corresponding fold_summary columns were
available. The fold_numeric_summary slot adapts
automatically to whichever numeric diagnostic columns are
present and does not have a dedicated flag.
The returned object includes (as gt tables by default):
identification_summary: top identified subgroups across
all sim x fold pairs, with counts and percentages.
grf_cut_summary: top raw grf_cuts strings across
training sets – how often GRF returned each output (and how often
it returned none). NULL if fold_summary$grf_cuts is
not available (e.g., objects from forestsearch prior to the
GLM-extension branch).
cut_vs_subgroup_xtab: cross-tabulation of unique
grf_cuts strings against identified subgroups. Reveals
whether GRF's discovery aligns with the pipeline's final selection.
no_subgroup_decomposition: tabulation of folds that
identified no subgroup, broken into "GRF returned no cut" vs
"GRF returned a cut but consistency rejected it".
pconsistency_distribution: binned distribution of the
consistency probability (Pcons) achieved by the identified
subgroup on each fold, restricted to folds that identified a
subgroup; includes median and IQR. Reveals whether identifying
folds are scraping pconsistency.threshold (e.g., Pcons just
above 0.90) or are well above it. NULL if
fold_summary$pconsistency is not available. Rejected-
candidate Pcons values are not surfaced – only the selected
candidate's Pcons is captured in fold_summary.
fold_numeric_summary: tidy median / IQR / range table
with one row per numeric diagnostic column present in
fold_summary (n_test, pconsistency,
training_fs_hr, n_candidates_evaluated). Absent
columns simply don't produce rows, so the table adapts to
pre-feature-branch objects without error. Training-fold
subgroup effects are on the effect measure's natural scale
(HR for survival; OR, RR, IRR, RD, IRD, or MD for GLM,
exponentiated at capture for ratio measures so the column is
always on natural scale regardless of outcome type); they are
in-sample estimates (optimistically biased) and are surfaced
for diagnostic comparison across folds only.
original_agreement: if original_sg and/or
original_grf_cuts are supplied, fraction of folds matching
the original full-data analysis (exact subgroup, partial shared
factor, GRF-cut match, both).
metrics_table: formatted version of
cv_output$sens_summary and cv_output$find_summary
(delegates to cv_metrics_tables).
plots: if create_plots = TRUE, bar charts of
top identified subgroups and top GRF cuts.
Raw data.frame versions of each tabulation are available at
$data$* for custom post-processing.
The GRF-dependent slots (grf_cut_summary,
cut_vs_subgroup_xtab, no_subgroup_decomposition) require
a grf_cuts column in fold_summary; the
pconsistency_distribution slot requires a pconsistency
column. Both were added on the feature/glm-extension branch
(grf_cuts first, pconsistency later). Older objects
are detected and the affected slots return NULL without error.
Metadata fields has_grf_cuts and has_pconsistency
record which columns were present.
forestsearch_tenfold for running repeated
K-fold CV; cv_metrics_tables for the underlying
metrics formatter; summarize_bootstrap_results for
the bootstrap analogue.
## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") df$time_months <- df$rfstime / 30.4375 fs <- forestsearch( df.analysis = df, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "time_months", event.name = "status", treat.name = "hormon" ) tf <- forestsearch_tenfold(fs.est = fs, sims = 100, Kfolds = 10) # Basic diagnostic summary cv_diag <- summarize_cv_results(tf) cv_diag$identification_summary cv_diag$grf_cut_summary cv_diag$no_subgroup_decomposition # With original-analysis comparison and plots cv_diag_full <- summarize_cv_results( tf, original_sg = fs$sg.harm, original_grf_cuts = fs$grf_cuts, create_plots = TRUE ) cv_diag_full$original_agreement cv_diag_full$plots$identification ## End(Not run)## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") df$time_months <- df$rfstime / 30.4375 fs <- forestsearch( df.analysis = df, confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er"), outcome.name = "time_months", event.name = "status", treat.name = "hormon" ) tf <- forestsearch_tenfold(fs.est = fs, sims = 100, Kfolds = 10) # Basic diagnostic summary cv_diag <- summarize_cv_results(tf) cv_diag$identification_summary cv_diag$grf_cut_summary cv_diag$no_subgroup_decomposition # With original-analysis comparison and plots cv_diag_full <- summarize_cv_results( tf, original_sg = fs$sg.harm, original_grf_cuts = fs$grf_cuts, create_plots = TRUE ) cv_diag_full$original_agreement cv_diag_full$plots$identification ## End(Not run)
Creates a summary table of operating characteristics across all simulations. Includes both HR and AHR metrics.
summarize_simulation_results( results, analyses = NULL, digits = 2, digits_hr = 3 )summarize_simulation_results( results, analyses = NULL, digits = 2, digits_hr = 3 )
results |
data.table with simulation results from run_simulation_analysis |
analyses |
Character vector. Analysis methods to include. Default: all |
digits |
Integer. Decimal places for proportions. Default: 2 |
digits_hr |
Integer. Decimal places for hazard ratios. Default: 3 |
Data frame with summary statistics
## Not run: dgm <- create_gbsg_dgm() sim_res <- run_simulation_analysis(dgm, nsim = 10) summarize_simulation_results(sim_res) ## End(Not run)## Not run: dgm <- create_gbsg_dgm() sim_res <- run_simulation_analysis(dgm, nsim = 10) summarize_simulation_results(sim_res) ## End(Not run)
Summary method for cox_ahr_cde objects
## S3 method for class 'cox_ahr_cde' summary(object, ...)## S3 method for class 'cox_ahr_cde' summary(object, ...)
object |
A |
... |
Additional arguments (not used). |
Invisibly returns the input object.
## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") res <- cox_ahr_cde_analysis(df, outcome.name = "rfstime", event.name = "status", treat.name = "hormon", confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er")) summary(res) ## End(Not run)## Not run: library(survival) df <- survival::gbsg df$grade3 <- as.integer(df$grade == "3") res <- cox_ahr_cde_analysis(df, outcome.name = "rfstime", event.name = "status", treat.name = "hormon", confounders.name = c("age", "meno", "size", "grade3", "nodes", "pgr", "er")) summary(res) ## End(Not run)
Builds a coefficient table with estimate, standard error, Wald z-statistic, and two-sided p-value for each coefficient.
## S3 method for class 'dina' summary(object, ...)## S3 method for class 'dina' summary(object, ...)
object |
an object of class |
... |
unused. |
an object of class "summary.dina", printable via
print().
Provides a detailed summary of a ForestSearch analysis including input parameters, variable selection results, consistency evaluation, and the selected subgroup with key metrics.
## S3 method for class 'forestsearch' summary(object, ...)## S3 method for class 'forestsearch' summary(object, ...)
object |
A |
... |
Additional arguments (currently unused). |
Invisibly returns object.
Carries the same block as print.forestsearch() (see there) and adds the
log-scale standard errors (field, field-s with the naive complement SE
beside it, and IJ two-term), the three largest re-selection frequencies
with their labels, and p_hat_sum over the re-selection family. Absent MR
results, the output is unchanged.
When the fit carries the membership-agreement diagnostics – that is, under
mr_inference_args = list(field_recovery = TRUE) – one further line
reports sens_H, the mean share of the identified subgroup that the field's
re-selections retain, beside . The pair separates two very
different low- situations: counts exact
re-selections of , so a low with a high sens_H
says the draws re-selected near-twins of , while a low
with a low sens_H says they re-selected unrelated regions.
Like it is descriptive and no construction reads it, and it
answers a narrower question than a bootstrap or cross-validation recovery
rate: re-selection within the fixed candidate family under perturbation, not
re-discovery from resampled data. The line is absent – and the rest of the
output byte-identical – when the diagnostics were not requested.
print.forestsearch() does not report it.
## Not run: fs <- forestsearch(df.analysis = mydata, ...) summary(fs) ## End(Not run)## Not run: fs <- forestsearch(df.analysis = mydata, ...) summary(fs) ## End(Not run)
fpr_calibration objectsDelegates to print.fpr_calibration.
## S3 method for class 'fpr_calibration' summary(object, ...)## S3 method for class 'fpr_calibration' summary(object, ...)
object |
An |
... |
Passed to the print method. |
The input object, invisibly.
Computes, from the raw matrices of a run_subgroup_sims() result (or
a loaded RDS payload coerced with class<-), every statistic used by
the vignettes' results table and forest panels: 1st/50th/99th
percentile matrices for the estimate and its upper bound, convergence
counts, mean subgroup N, tail probabilities, unconditional median
display strings, the formatted results_tbl, category labels and
colours, validity masks, the single/combination panel index vectors,
and the high-risk panel (threshold filter, ITT anchor, ordering).
## S3 method for class 'subgroup_sims' summary( object, hr_true = object$hr_true, probs = c(0.01, 0.5, 0.99), est_thresholds = NULL, ub_thresholds = NULL, est_label = NULL, ub_label = NULL, single_cats = c("ITT", "Clinical", "Continuous"), combo_cats = c("Interaction", "3-way", "Random"), pr_thresh_highrisk = 0.1, itt_name = "All Patients", cat_cols = c(ITT = "black", Clinical = "steelblue", Continuous = "darkgreen", Interaction = "purple4", `3-way` = "violetred3", Random = "darkorange"), ... )## S3 method for class 'subgroup_sims' summary( object, hr_true = object$hr_true, probs = c(0.01, 0.5, 0.99), est_thresholds = NULL, ub_thresholds = NULL, est_label = NULL, ub_label = NULL, single_cats = c("ITT", "Clinical", "Continuous"), combo_cats = c("Interaction", "3-way", "Random"), pr_thresh_highrisk = 0.1, itt_name = "All Patients", cat_cols = c(ITT = "black", Clinical = "steelblue", Continuous = "darkgreen", Interaction = "purple4", `3-way` = "violetred3", Random = "darkorange"), ... )
object |
A |
hr_true |
True effect on the fitter's estimate scale, for
reference lines (a hazard ratio for Cox fits, a mean difference
for |
probs |
Quantile probabilities for the ECI matrices; the default
|
est_thresholds |
Length-2 numeric |
ub_thresholds |
Length-2 numeric: the upper-bound tails are
|
est_label, ub_label
|
Display labels for the estimate and its
upper bound in the table headers (legacy |
single_cats, combo_cats
|
Category labels routed to the single-variable and combination forest panels. |
pr_thresh_highrisk |
High-risk panel inclusion threshold on
the first upper-bound tail probability, default |
itt_name |
Name of the ITT row, always included in and anchored at the top of the high-risk panel. |
cat_cols |
Named colour map for categories. |
... |
Unused. |
Field names are structural and retained across outcome types
(hr_q, pr_hr_lt050, pr_ub_ge2, ...), exactly as sim_hrs and
hr.threshold serve generic duty elsewhere in the package: on a GLM
result they hold the fitter's estimate-scale statistics. Estimates
follow the estimator layer's harm-positive normalization, so the
upper tail (Pr(est > null), Pr(UB >= t)) always points toward
harm whatever the raw outcome's direction.
Threshold and label resolution: explicit arguments win; otherwise the
result's effect metadata supplies them (subgroup_glm() stamps
scale-aware defaults from Phase 4.4 – ratio measures carry the
legacy tails c(0.5, 1) / c(2, 3), identity measures
c(NA, null_value) / c(NA, NA) – with labels from the effect
measure); otherwise the HR legacy values
c(0.5, 1.0) / c(2, 3) with the vignettes' literal headers. An
NA threshold degrades gracefully: its probability field is NA
and its table column renders "-" under a placeholder header.
Structurally empty subgroups (all-NA columns) yield NA medians
and NaN means/proportions, exactly as in the vignettes; the
formatted table renders them as "-".
An object of class "subgroup_sims_summary": a list with
fields named as in the vignettes (hr_q, ub_q, n_valid,
pct_valid, mean_n, pr_hr_lt050, pr_hr_gt1, pr_ub_ge2,
pr_ub_ge3, mhr_uncond, mub_uncond, results_tbl,
sg_names, cat, cat_cols, ok, ok_ub, idx_hr_single,
idx_hr_combo, idx_ub_single, idx_ub_combo, n_single,
n_combo, highrisk, n_sims, design, hr_true). Legacy
calls return exactly this set; effect-aware calls append effect
(the result's metadata, possibly NULL when only explicit
overrides were given), thresholds (list(est =, ub =) as
resolved), and labels (list(est =, ub =)).
run_subgroup_sims(), subgroup_glm()
Creates summary tables from MRCT simulation results using the gt package. Summarizes hazard ratio estimates, subgroup identification rates, and classification of identified subgroups. Optionally displays two scenarios (e.g., alternative and null hypotheses) side by side.
summaryout_mrct( pop_summary = NULL, mrct_sims, mrct_sims_null = NULL, scenario_labels = c("Alternative", "Null"), pop_summary_null = NULL, sg_type = 1, tab_caption = "Identified subgroups and estimation summaries", digits = 3, trim_threshold = 1000, trim_fraction = 0.01, table_width = 600, font_size = 11, showtable = TRUE )summaryout_mrct( pop_summary = NULL, mrct_sims, mrct_sims_null = NULL, scenario_labels = c("Alternative", "Null"), pop_summary_null = NULL, sg_type = 1, tab_caption = "Identified subgroups and estimation summaries", digits = 3, trim_threshold = 1000, trim_fraction = 0.01, table_width = 600, font_size = 11, showtable = TRUE )
pop_summary |
List. Population summary from large sample approximation (optional). Default: NULL |
mrct_sims |
data.table. Simulation results from
|
mrct_sims_null |
data.table. Optional second set of simulation results (e.g., null hypothesis). When supplied, the table displays two value columns side by side. Default: NULL (single-scenario table). |
scenario_labels |
Character vector of length 2. Column headers for the
two scenarios. Only used when |
pop_summary_null |
List. Population summary for the null scenario (optional). Default: NULL |
sg_type |
Integer. Type of subgroup summary: 1 = basic summary (found, biomarker, age); 2 = extended summary (all subgroup types). Default: 1 |
tab_caption |
Character. Caption for the output table. Default: "Identified subgroups and estimation summaries" |
digits |
Integer. Number of decimal places for numeric summaries. Default: 3 |
trim_threshold |
Numeric. When the raw mean of a metric exceeds this
value in absolute terms, the summary switches to a symmetrically trimmed
mean and SD (excluding the lower and upper |
trim_fraction |
Numeric between 0 and 0.5. Fraction of observations to trim from each tail when trimming is triggered. Default: 0.01 (1 percent from each tail, i.e., the central 98 percent of values). |
table_width |
Numeric. Total table width in pixels. Column widths are allocated proportionally. Increase for HTML/wide displays (e.g., 750), decrease for beamer slides (e.g., 550). Default: 600. |
font_size |
Numeric. Base font size in pixels. Title is
|
showtable |
Logical. Print the table. Default: TRUE |
List with components:
List of summary statistics from population. When
dual-scenario, contains res_alt and res_null.
Formatted gt table object, or data.frame if gt is unavailable.
Processed mrct_sims data.table with derived variables.
When dual-scenario, also contains data_null.
Data frame of computed summary statistics.
mrct_region_sims for generating simulation results
## Not run: # Single scenario (backward-compatible) summaryout_mrct( mrct_sims = results_alt, tab_caption = "H1: Heterogeneous treatment effect" ) # Dual scenario: alternative vs null side-by-side summaryout_mrct( mrct_sims = results_alt, mrct_sims_null = results_null, scenario_labels = c("Alternative (HTE)", "Null (uniform)"), tab_caption = "Operating characteristics: Alternative vs Null" ) # Custom trimming: 2% from each tail when mean > 500 summaryout_mrct( mrct_sims = results_alt, mrct_sims_null = results_null, trim_threshold = 500, trim_fraction = 0.02 ) ## End(Not run)## Not run: # Single scenario (backward-compatible) summaryout_mrct( mrct_sims = results_alt, tab_caption = "H1: Heterogeneous treatment effect" ) # Dual scenario: alternative vs null side-by-side summaryout_mrct( mrct_sims = results_alt, mrct_sims_null = results_null, scenario_labels = c("Alternative (HTE)", "Null (uniform)"), tab_caption = "Operating characteristics: Alternative vs Null" ) # Custom trimming: 2% from each tail when mean > 500 summaryout_mrct( mrct_sims = results_alt, mrct_sims_null = results_null, trim_threshold = 500, trim_fraction = 0.02 ) ## End(Not run)
H0': tau(x) = tau for some tau and all x.
test_constant_cate( Y, W, X, poly_order = 1L, covariate_select = c("all", "top_down", "bottom_up"), t_threshold = 2, regression = c("ols", "logistic", "poisson"), offset = NULL )test_constant_cate( Y, W, X, poly_order = 1L, covariate_select = c("all", "top_down", "bottom_up"), t_threshold = 2, regression = c("ols", "logistic", "poisson"), offset = NULL )
Y |
Numeric outcome vector. |
W |
Treatment indicator (0/1). |
X |
Numeric covariate matrix (no factors; use
|
poly_order |
Integer. Default: 1. |
covariate_select |
"all", "top_down", or "bottom_up". |
t_threshold |
Numeric. Default: 2.0. |
regression |
"ols", "logistic", or "poisson". |
offset |
Numeric offset vector (Poisson only). |
A list with components test, chi_sq, df,
p_value_chi, normal (normal-approximation statistic),
p_value_normal, K (number of covariates), N0,
N1, regression, covariates_selected,
ate_diff (intercept-difference point estimate), and
diff_slope, V_slope. Returns NULL if per-arm
fits fail or fewer than two covariates are retained.
Run Both Crump et al. Tests
test_hte( Y, W, X, poly_order = 1L, covariate_select = c("all", "top_down", "bottom_up"), t_threshold = 2, regression = c("ols", "logistic", "poisson"), offset = NULL )test_hte( Y, W, X, poly_order = 1L, covariate_select = c("all", "top_down", "bottom_up"), t_threshold = 2, regression = c("ols", "logistic", "poisson"), offset = NULL )
Y |
Numeric outcome vector. |
W |
Treatment indicator (0/1). |
X |
Numeric covariate matrix (no factors; use
|
poly_order |
Integer. Default: 1. |
covariate_select |
"all", "top_down", or "bottom_up". |
t_threshold |
Numeric. Default: 2.0. |
regression |
"ols", "logistic", or "poisson". |
offset |
Numeric offset vector (Poisson only). |
Object of class hte_test.
HTE Test Using ForestSearch-Selected Covariates
test_hte_from_forestsearch( fs_result, df, outcome.name, treat.name, regression = c("ols", "logistic", "poisson"), offset = NULL, poly_order = 1L )test_hte_from_forestsearch( fs_result, df, outcome.name, treat.name, regression = c("ols", "logistic", "poisson"), offset = NULL, poly_order = 1L )
fs_result |
A |
df |
Data frame used in the |
outcome.name |
Character. Outcome variable name. |
treat.name |
Character. Treatment variable name. |
regression |
Character: "ols", "logistic", or "poisson". |
offset |
Numeric offset vector (Poisson only). |
poly_order |
Integer. Default: 1. |
List with hte_test, covariates_used, source.
H0: tau(x) = 0 for all x.
test_zero_cate( Y, W, X, poly_order = 1L, covariate_select = c("all", "top_down", "bottom_up"), t_threshold = 2, regression = c("ols", "logistic", "poisson"), offset = NULL )test_zero_cate( Y, W, X, poly_order = 1L, covariate_select = c("all", "top_down", "bottom_up"), t_threshold = 2, regression = c("ols", "logistic", "poisson"), offset = NULL )
Y |
Numeric outcome vector. |
W |
Treatment indicator (0/1). |
X |
Numeric covariate matrix (no factors; use
|
poly_order |
Integer. Default: 1. |
covariate_select |
"all", "top_down", or "bottom_up". |
t_threshold |
Numeric. Default: 2.0. |
regression |
"ols", "logistic", or "poisson". |
offset |
Numeric offset vector (Poisson only). |
List with test results including covariates_selected.
Checks that input data meets requirements for GRF analysis
validate_grf_data(W, D, n.min)validate_grf_data(W, D, n.min)
W |
Numeric vector. Treatment indicator |
D |
Numeric vector. Event indicator |
n.min |
Integer. Minimum subgroup size |
Logical. TRUE if data is valid, FALSE with warning otherwise
W <- rep(0:1, each = 50) D <- rbinom(100, 1, 0.6) validate_grf_data(W, D, n.min = 60)W <- rep(0:1, each = 50) D <- rbinom(100, 1, 0.6) validate_grf_data(W, D, n.min = 60)
Test function to verify that k_inter properly modulates the difference between HR(H) and HR(Hc), and that AHR metrics align with Cox-based HRs.
validate_k_inter_effect( k_inter_values = c(-2, -1, 0, 1, 2, 3), verbose = TRUE, ... )validate_k_inter_effect( k_inter_values = c(-2, -1, 0, 1, 2, 3), verbose = TRUE, ... )
k_inter_values |
Numeric vector of k_inter values to test. Default: c(-2, -1, 0, 1, 2, 3) |
verbose |
Logical. Print results. Default: TRUE |
... |
Additional arguments passed to create_gbsg_dgm |
Data frame with k_inter, hr_H, hr_Hc, AHR_H, AHR_Hc, and ratio columns
## Not run: # Test k_inter effect results <- validate_k_inter_effect() # k_inter = 0 should give hr_H approximately equals hr_Hc (ratio approximately 1) ## End(Not run)## Not run: # Test k_inter effect results <- validate_k_inter_effect() # k_inter = 0 should give hr_H approximately equals hr_Hc (ratio approximately 1) ## End(Not run)
Checks that a dataset contains all required variables for MRCT simulation functions and reports any issues. Required variables include outcome (tte, event), treatment (treat), continuous covariates (age, bm), and factor covariates (male, histology, prior_treat, regA).
validate_mrct_data(df.case, verbose = TRUE)validate_mrct_data(df.case, verbose = TRUE)
df.case |
Data frame to validate |
verbose |
Logical. Print detailed validation results. Default: TRUE |
The function checks for the following variables:
Outcome: tte (time-to-event), event (0/1 indicator)
Treatment: treat (0/1 indicator)
Continuous: age, bm (biomarker)
Factor: male (0/1), histology, prior_treat (0/1), regA (0/1)
The function also validates variable types and value ranges.
Logical. TRUE if all requirements met, FALSE otherwise (invisibly)
create_dgm_for_mrct for creating DGM from validated data
## Not run: # Check if dataset is ready for MRCT simulations is_valid <- validate_mrct_data(df.case) if (!is_valid) { stop("Please fix data issues before running simulations") } ## End(Not run)## Not run: # Check if dataset is ready for MRCT simulations is_valid <- validate_mrct_data(df.case) if (!is_valid) { stop("Please fix data issues before running simulations") } ## End(Not run)
Evaluates every subgroup id expression against data (augmented
with flag_itt = 1L, the scalar cutpoints as columns, and 0-valued
benchmark membership columns) and fails with a named, collected error
if any expression errors, is non-logical, or has the wrong length.
Run automatically at the top of run_subgroup_sims() so that a typo
in a subgroup definition stops the study in milliseconds instead of
surfacing as a silent all-NA column after the simulation loop.
validate_subgroups( subgroups, data, cutpoints = list(), benchmarks = benchmark_spec() )validate_subgroups( subgroups, data, cutpoints = list(), benchmarks = benchmark_spec() )
subgroups |
List of subgroup definitions; each element a list
with character scalars |
data |
A data frame carrying the covariates the |
cutpoints |
Named list of scalars exposed as columns (e.g.
|
benchmarks |
A |
Invisibly TRUE; errors otherwise.
Computes Wilson score confidence interval for a proportion, which has better coverage properties than the normal approximation for small samples and proportions near 0 or 1.
wilson_ci(x, n, conf.level = 0.95)wilson_ci(x, n, conf.level = 0.95)
x |
Integer. Number of successes. |
n |
Integer. Number of trials. |
conf.level |
Numeric. Confidence level (default 0.95). |
Named numeric vector with elements: estimate, lower, upper.
wilson_ci(90, 100) wilson_ci(5, 20, conf.level = 0.90)wilson_ci(90, 100) wilson_ci(5, 20, conf.level = 0.90)