Provides Markov chain Monte Carlo (MCMC) sampling and genetic algorithms for searching combinations of nodes in hierarchical trees. Includes scores for binary outcomes, continuous outcomes, and recurrent events, with applications to pharmacovigilance and hospital data. Importance weighting estimates uniform combination score references from tempered MCMC samples and reports weight-based effective sample sizes. Computational routines are implemented in C++ via 'Rcpp'.
treehunt is an R package that implements evolutionary optimization algorithms to search for optimal node combinations in hierarchical tree structures. Originally developed for pharmacovigilance applications to identify drug (ATC code) combinations associated with adverse events, the package generalizes to any tree hierarchy and optimization metric.
Finding optimal combinations of elements from a hierarchical structure is a challenging combinatorial problem. treehunt addresses this by providing two algorithms:
Both algorithms are implemented in C++ via Rcpp for high performance and support multiple scoring functions.
# install.packages("devtools")
devtools::install_github("JulesBa-Git/treehunt")
library(treehunt)
# Create example patient data
set.seed(42)
n_patients <- 500
patient_df <- data.frame(
patient_id = 1:n_patients,
adverse_event = rbinom(n_patients, 1, 0.2)
)
# Each patient has a list of tree node indices
patient_df$drug_codes <- lapply(1:n_patients, function(i) {
sample(5:25, size = sample(1:4, 1), replace = FALSE)
})
# Define tree structure: depth vector where index = node, value = depth
# Example: 3-level tree with 1 root, 5 level-2 nodes, each with 4 level-3 children
tree_depth <- c(1, # Node 0: root
2, 3, 3, 3, 3, # Node 1 (child of 0) + its 4 children
2, 3, 3, 3, 3, # Node 6 (child of 0) + its 4 children
2, 3, 3, 3, 3, # Node 11 (child of 0) + its 4 children
2, 3, 3, 3, 3, # Node 16 (child of 0) + its 4 children
2, 3, 3, 3, 3) # Node 21 (child of 0) + its 4 children
# Run MCMC search
mcmc_results <- run_mcmc(
patient_data = patient_df,
node_column = "drug_codes", # or by index : 3
target_column = "adverse_event", # or by index : 2
tree_depth = tree_depth,
epochs = 10000,
cocktail_size = 2,
score_type = "hypergeometric",
seed = 4601L,
verbose = TRUE
)
# View top results
print(mcmc_results$top_scores)
print(mcmc_results$top_solutions)
The MCMC target favours high scores: f_T(C) is proportional to
exp(S(C) / temperature). Here S is capped at max_score, as in the
Metropolis-Hastings acceptance step. The reference below gives equal weight to
each observed combination of the chosen size; different score values need not
have equal probabilities.
reference_run <- run_mcmc(
patient_data = patient_df, node_column = "drug_codes",
target_column = "adverse_event", tree_depth = tree_depth,
epochs = 10000, burn_in = 1000, cocktail_size = 2,
temperature = 2, max_score = 200, beta = 4, seed = 4601L,
store_trace = TRUE
)
reference_run$uniform_reference$distribution
reference_run$uniform_reference$weight_ess
reference_run$uniform_reference_filtered$weight_ess
# Exact score values instead of histogram bins, using the optional trace:
reference <- uniform_score_reference(reference_run$trace$score, temperature = 2)
reference$distribution
At every retained iteration, including repeats after rejection, the inverse
target weight is proportional to exp(-S / temperature). The engine computes
these weights before binning, then returns normalized bin probabilities, their
CDF and inclusive upper tails. The filtered reference conditions on coverage
strictly greater than beta (distinct patients for PWP). An empty filtered
reference returns n_samples = 0 and NA probabilities and ESS.
weight_ess = sum(w)^2 / sum(w^2) measures weight concentration. It does not
account for MCMC autocorrelation or establish convergence. For large score
ranges relative to temperature, a longer run or a higher temperature may be
needed to cover low-score regions. Check mixing separately. With a finite cap,
the reference describes capped scores; scores beyond the cap are grouped together.
By default store_trace = FALSE: weighted histograms and ESS are accumulated
without storing the full trajectory. burn_in is a number of initial iterations
discarded from distributions, top solutions and trace. Acceptance statistics
still describe all iterations. Legacy histograms alone cannot be reweighted
exactly because they do not retain individual scores within each bin.
The MCMC state space contains observed fixed-size subsets of distinct nodes,
including ancestor-descendant pairs. Node vectors in observations, MCMC outputs
and traces are zero-based tree indices. The support condition and tree must
match when comparing a sampled reference with
mcmc_size_2_true_score_distribution(); that function enumerates supported pairs
with unit weights. The GA enforces its own ancestor-descendant validity rule.
# Run genetic algorithm search
ga_results <- run_genetic_algorithm(
patient_data = patient_df,
node_column = "drug_codes",
target_column = "adverse_event",
tree_depth = tree_depth,
population_size = 100,
epochs = 500,
mutation_rate = 0.1,
elite_count = 2,
score_type = "hypergeometric",
seed = 4602L,
verbose = TRUE
)
# Inspect the ten highest scores and their zero-based node indices
top <- head(order(ga_results$final_scores, decreasing = TRUE), 10)
data.frame(
score = ga_results$final_scores[top],
nodes = vapply(ga_results$final_population[top], paste,
collapse = ",", FUN.VALUE = character(1))
)
The generic GA and MCMC wrappers seed their C++ random-number generators
directly. Supply seed to reproduce a stochastic run; the default NULL
preserves non-deterministic initialization. Calling set.seed() alone does not
set these C++ streams.
Tests whether the node (drug) combination is significantly over-represented among patients with the adverse event using the hypergeometric distribution:
$$ \text{score} = -\log(P(X ≥ k)) $$
where k is the number of patients with both the combination and the adverse event.
Calculates the ratio of adverse event probability in patients with the combination versus those without:
$$ RR =\frac{\mathbb{P}(\text{event}| \text{combination})}{\mathbb{P}(\text{event} | \text{no combination})} $$
If you need a scoring function that isn't currently implemented, we'd appreciate to hear from you! You can:
The tree structure is defined by a depth vector where:
Example: A tree with structure:
0 (root)
/|\
1 4 5
/| |
2 3 6
Would be defined as:
tree_depth <- c(1, 2, 3, 3, 2, 2, 3)
For pharmacovigilance applications with ATC codes:
# ATC codes have five levels (code lengths 1, 3, 4, 5, 7).
# Example path, already in depth-first order:
atc_data <- data.frame(code = c("A", "A01", "A01A", "A01AA", "A01AA01"))
atc_data$depth <- match(nchar(atc_data$code), c(1L, 3L, 4L, 5L, 7L))
treehunt can rank hierarchical code combinations for recurrent-event data
with an exact Efron score test from a stratified PWP gap-time Cox model. Fit the
reduced model once, then reuse its context throughout the search:
context <- fit_pwp_rao_context(
data = pwp_data,
status_column = "status_any", # or "status_emergency"
covariates = c("age_start", "index_emergency", "year_start"),
min_covered_patients = 20L,
min_covered_events = 5L
)
ga <- run_pwp_genetic_algorithm(
data = pwp_data,
context = context,
tree = icd10_tree,
node_column = "icd10_indices",
seed = 20260816L
)
details <- score_pwp_combinations(
combinations = ga$final_population,
data = pwp_data,
context = context,
tree = icd10_tree,
index_base = 0L
)
The interval list-column and tree UpperBound must both use zero-based node
indexes. In particular, when a CSV stores an inclusive one-based subtree bound,
use UpperBound <- UpperBound - 1L. The screening fitness is
max(0, signed_z); details retains the signed Rao statistic, model-based
p-value, coverage counts, and efficient information. Use
refit_pwp_combinations() to obtain patient-clustered robust Cox estimates for
the selected candidates.
mcmc_results <- run_mcmc(...)
mcmc_results$top_solutions # List of node vectors
mcmc_results$top_scores # Corresponding scores
mcmc_results$top_solutions_filtered # Solutions meeting beta threshold
mcmc_results$score_distribution # Histogram of visited scores
mcmc_results$score_distribution_filtered # Histogram of visited scores
# meeting beta threshold
mcmc_results$statistics # Acceptance rates, move counts
ga_results <- run_genetic_algorithm(...)
ga_results$final_population # Complete final population
ga_results$statistics # Generations, cache hits
For more details about the method you can watch our preprint:
Contributions are welcome! Please feel free to submit issues and pull requests.
git checkout -b feature/feature_name)git commit -m 'Add feature_name: details')git push origin feature/feature_name)GPL-3. See the repository LICENSE file.