hOUwie performance and stability improvements

knitr::opts_chunk$set(
  echo = FALSE,
  warning = FALSE,
  message = FALSE,
  fig.align = "center",
  out.width = "90%"
)

performance <- data.frame(
  scenario = c(
    "Moderate tree",
    "More histories",
    "1,000-tip tree",
    "16 states / 74 parameters",
    "500 tips / 8 states"
  ),
  tips = c(96, 96, 1000, 250, 500),
  histories = c(25, 100, 25, 25, 50),
  states = c(4, 4, 4, 16, 8),
  parameters = c(8, 8, 8, 74, 22),
  previous_seconds = c(0.335, 1.283, 4.713, 1.908, 5.513),
  updated_seconds = c(0.131, 0.463, 1.372, 0.365, 1.267),
  stringsAsFactors = FALSE
)
performance$speedup <- performance$previous_seconds /
  performance$updated_seconds

performance_table <- data.frame(
  Scenario = performance$scenario,
  Tips = performance$tips,
  Histories = performance$histories,
  States = performance$states,
  Parameters = performance$parameters,
  `Previous version (s)` = sprintf("%.3f", performance$previous_seconds),
  `Updated version (s)` = sprintf("%.3f", performance$updated_seconds),
  Speedup = sprintf("%.2fx", performance$speedup),
  check.names = FALSE
)

hOUwie now runs between r sprintf("%.2f", min(performance$speedup)) and r sprintf("%.2f", max(performance$speedup)) times faster across the benchmark set. The largest improvements appear on large trees and models with many states and parameters.

How much faster is it?

knitr::kable(
  performance_table,
  align = c("l", rep("r", 7)),
  caption = "Median elapsed time across repeated runs. Lower times are better."
)
bar_colors <- ifelse(performance$speedup >= 4, "#2b8cbe", "#7bccc4")
old_par <- par(mar = c(8, 4.2, 1, 0.5))
bars <- barplot(
  performance$speedup,
  names.arg = performance$scenario,
  las = 2,
  ylim = c(0, max(performance$speedup) * 1.18),
  ylab = "Speedup (times faster)",
  col = bar_colors,
  border = NA
)
abline(h = 1, lty = 2, col = "grey50")
text(
  bars,
  performance$speedup,
  labels = sprintf("%.2fx", performance$speedup),
  pos = 3,
  cex = 0.9
)
par(old_par)

The moderate 96-tip analysis is 2.6 times faster. Increasing the number of histories from 25 to 100 raises that to 2.8 times. On the larger and more parameter-rich cases, the updated implementation is 3.4 to 5.2 times faster. A 1,000-tip run falls from 4.713 seconds to 1.372 seconds, while the 16-state, 74-parameter case falls from 1.908 seconds to 0.365 seconds.

Every run produced the requested number of stochastic histories. The median size of the returned history objects changed by less than one percent in every case.

Where the speedup comes from

Most of the improvement comes from preparing the tree structure once and reusing it. A phylogenetic tree has the same topology throughout a fit. The updated code prepares its paths, descendants, edge positions, and state relationships up front, then keeps compact lookup tables for the calculations that follow.

Together, these changes remove a large amount of repeated work from the inner likelihood loop. The 16-state analysis benefits most: the updated implementation spends more of its time on the statistical calculation and less on repeatedly organizing the same tree.

A steadier optimization problem

hOUwie estimates a likelihood by averaging over sampled evolutionary histories. That sampling introduces a little numerical movement from one evaluation to the next. An optimizer can mistake that movement for a meaningful improvement and spend time following noise.

The updated implementation compares candidate parameter values with a shared underlying random stream within each optimizer start. The histories still respond to the candidate parameters; they are not frozen. The shared stream simply makes one candidate's likelihood more directly comparable with the next. This behavior is enabled by default and can be controlled with the common_random_numbers argument to hOUwie().

On an easy example, both approaches found the same answer. On a more difficult hidden-state model, the steadier comparison produced better and more consistent fits on fresh evaluations:

| Measure | Ordinary sampling | Shared random stream | |:--|--:|--:| | Mean fresh-evaluation log likelihood | -27.9244 | -27.2323 | | Median fresh-evaluation log likelihood | -28.0759 | -27.1515 | | Between-run standard deviation | 0.6145 | 0.2669 | | Mean likelihood evaluations | 524.0 | 438.5 | | Mean elapsed time (seconds) | 75.24 | 61.48 |

The steadier search used 16.3% fewer likelihood evaluations and 18.3% less wall time. It found the better fresh-evaluated solution in four of six paired starts. Multiple starts remain important when hidden states create weakly identified parameters or local optima.

Reliability improvements around the likelihood

The fitting path also includes the following reliability improvements:

The expanded suite contains 235 expectations across 80 tests, covering the faster execution path and these edge cases.



Try the OUwie package in your browser

Any scripts or data that you put into this service are public.

OUwie documentation built on Sept. 17, 2026, 1:09 a.m.