Adaptive Optimal Stopping Rule Algorithms for Efficient Data Collection and Analysis
optstop is a Python package implementing adaptive optimal stopping algorithms for both post-hoc (batch) and live (incremental) data collection scenarios. It is designed to help researchers and practitioners efficiently determine when enough data has been collected to make reliable inferences, saving time and resources.
This repository is the reference implementation accompanying the paper:
Pilditch, T. D. (2026). Knowing When to Stop: Bayesian Optimal Stopping for LLM Evaluations. arXiv:2608.14425. Presented at the ICML 2026 Workshop on Statistical Frameworks for Uncertainty in Agentic Systems.
The paper PDF, along with supporting simulation scripts, validation outputs, and reproducibility materials, is in the paper/ directory.
If you use optstop in your research, please cite the paper:
@misc{pilditch2026knowing,
title={Knowing When to Stop: {B}ayesian Optimal Stopping for {LLM} Evaluations},
author={Pilditch, Toby D.},
year={2026},
eprint={2608.14425},
archivePrefix={arXiv},
primaryClass={cs.AI},
doi={10.48550/arXiv.2608.14425},
note={Presented at the ICML 2026 Workshop on Statistical Frameworks for Uncertainty in Agentic Systems}
}- Post-hoc (batch) optimal stopping for retrospective analysis and dataset pruning
- Live (incremental) optimal stopping for real-time data collection
- Flexible, parameterised stopping criteria
- Flexible column mapping for groupings, sample IDs, and epochs
- Bayesian and frequentist hybrid methodology
- GPU acceleration via JAX/numpyro for faster PyMC sampling
- Ordinal scoring support for ordinal data (e.g., 0-10), bounded continuous support (e.g., for bounded aggregates), in addition to binary (0/1) scoring
- Convergence projection: estimates additional trials needed when evaluation ends before convergence
- inspect_ai integration for LLM evaluation workflows with adaptive early stopping
optstop is not yet published on PyPI - install directly from GitHub:
pip install "git+https://github.com/UKGovernmentBEIS/optstop.git" # Core package
pip install "optstop[inspect] @ git+https://github.com/UKGovernmentBEIS/optstop.git" # With inspect_ai integration
pip install "optstop[gpu] @ git+https://github.com/UKGovernmentBEIS/optstop.git" # With GPU acceleration (NVIDIA/Apple Silicon)Once optstop is published to PyPI the pip install optstop form (with the same extras) will also work.
Reproducible install: dependencies are specified with lower bounds only (with a single cap, arviz<1.0, which also holds pymc<6). For the exact validated dependency set, install against the committed lockfile after cloning:
pip install -r requirements-lock.txtSee detailed installation options for development setup and platform-specific notes.
Using optstop with inspect_ai? See the inspect_ai integration guide below, and the full
BRIDGE_API_REFERENCE.mdfor theOptimalStoppingManagerAPI.
The most common use case: you have evaluation results in a CSV or DataFrame and want to determine how much of that data was actually needed.
Your data must be in long format - one row per observation. You then tell optstop which columns play which roles:
| Column role | What it means | Example columns |
|---|---|---|
| Grouping | Any column(s) representing factors that may systematically affect performance. Stopping decisions are made independently per unique combination. A good rule of thumb: include any factor you are manipulating or comparing (e.g., model, task, token budget). | model, benchmark, max_tokens |
| Sample ID | Individual test items within a grouping. | question_id |
| Epoch | Repeated evaluations of the same item (integers, processed in ascending order). | run_number |
| Score | The outcome. Binary (0/1), ordinal integers (e.g., 0-10), or continuous floats. | accuracy, rating |
You can use any columns from your data as groupings - they don't need special names. If you specify multiple grouping columns, each unique combination becomes an independent analysis (e.g., ['model', 'benchmark'] with 2 models and 3 benchmarks yields 6 groupings).
Example CSV structure:
model, benchmark, question_id, run, accuracy
gpt-4o, mmlu, q_001, 1, 1
gpt-4o, mmlu, q_001, 2, 1
gpt-4o, mmlu, q_002, 1, 0
gpt-4o, mmlu, q_002, 2, 1
gpt-4o, gpqa, q_101, 1, 0
claude-4, mmlu, q_001, 1, 1
...
import pandas as pd
from optstop import optimal_stopping_posthoc
df = pd.read_csv('my_eval_results.csv')
pruned_df, summary = optimal_stopping_posthoc(
df,
params={}, # Use defaults (delta=0.05, 97% CIs)
grouping_columns=['model', 'benchmark'], # Each model-benchmark pair analysed independently
sample_id_column='question_id',
epoch_column='run',
score_column='accuracy'
)This analyses each grouping (e.g., gpt-4o-mmlu, gpt-4o-gpqa, claude-4-mmlu) independently, determining where data collection could have safely stopped.
Runtime: Each grouping runs Bayesian MCMC inference, so analysis takes longer than simple summary statistics. Runtime scales with the number of groupings, items, and epochs. Progress is logged by default.
for entry in summary:
g = entry['grouping']
eff = entry['percent_items_used']
width = entry['theta_ci_width']
print(f"{g}: used {eff:.1%} of data, final CI width {width:.3f}")
# For groupings that didn't converge, check the projection
if entry.get('convergence_projection'):
proj = entry['convergence_projection']
print(f" -> ~{proj['simple_proj_additional_trials']:.0f} more trials estimated to converge")pruned_df contains only the rows up to each grouping's stopping point - you can use this directly for downstream analysis, knowing the precision target was met.
Key fields in each summary entry:
| Field | Description |
|---|---|
grouping |
Grouping name (hyphen-joined from grouping columns) |
theta_ci_low, theta_ci_high |
Credible interval bounds on group performance (a point estimate, if needed, is the interval midpoint) |
theta_ci_width |
CI width (lower = more precise) |
percent_items_used |
Fraction of items evaluated before stopping (0-1; format with :.1%) |
n_items_used |
Number of items evaluated |
avg_reps_per_item |
Mean epochs per item at stopping |
convergence_projection |
For non-converged groupings: estimated additional trials needed (dict or None) |
low_perf_stop_suppressed |
True if an automatic stop was suppressed because observed performance was below low_performance_threshold - the low-base-rate safeguard, so all available samples were used (issue #3) |
pinned |
Binary/continuous only: True when the whole reported credible interval sits below the resolution floor, so the bounds are prior-dominated and qualitative (issue #3). False for ordinal |
low_perf_floor |
Ordinal counterpart to pinned (telemetry only, does not affect stopping): True when the resolved normalised performance is below low_performance_threshold. False for binary/continuous |
boundary_diagnostic |
Warning string if performance resolved near the floor/ceiling, else None |
error |
Error message if inference failed for this grouping (None if successful) |
Score types: By default, all groupings use binary (0/1) inference. For ordinal rubric scores, specify which groupings are ordinal:
pruned_df, summary = optimal_stopping_posthoc(
df, params={},
grouping_columns=['model', 'benchmark'],
sample_id_column='question_id',
epoch_column='run',
score_column='rating',
ordinal_tasks=['writing'], # Groupings whose name contains 'writing' use ordinal inference
# (matched against the hyphen-joined grouping key, case-insensitive)
ordinal_max_score=10, # Rating scale is 0-10
ordinal_inference='hybrid', # Recommended mode (modal CI + entropy convergence)
)Ordinal score indexing. The package models categories 0 through
ordinal_max_score. If your rubric uses 1-5, you can either subtract 1 from scores before analysis (df['rating'] = df['rating'] - 1, withordinal_max_score=4), or setordinal_max_score=5and let category 0 remain empty. Both approaches produce correct modal estimates with sufficient data; the latter may trigger a sparsity warning for the unused category.
Precision threshold: The delta_item and delta_cap parameters control how precise estimates must be before stopping. Lower values require more data but give tighter estimates:
params = {
'delta_item': 0.05, # Item-level CI width target (default: 0.05)
'delta_cap': 0.05, # Group-level CI width target (default: 0.05)
}Shuffling: If your items are sorted by difficulty, shuffle before analysis to satisfy the exchangeability assumption:
pruned_df, summary = optimal_stopping_posthoc(
df, params={},
grouping_columns=['model', 'benchmark'],
sample_id_column='question_id',
epoch_column='run',
score_column='accuracy',
shuffle_items=True,
shuffle_seed=42
)For the full parameter reference, see Setting Parameters. For logging configuration, diagnostic plots, and advanced usage, see the sections below.
optstop integrates with inspect_ai, the UK AI Security Institute's framework for LLM evaluations. The OptimalStoppingManager implements the EarlyStopping protocol, adding Bayesian adaptive early stopping to your LLM evaluations.
For those using this package in conjunction with inspect_ai, please see BRIDGE_API_REFERENCE.md for more relevant documentation.
Running LLM evaluations can be expensive and time-consuming:
- API costs: Each evaluation sample costs money (especially for large models)
- Compute time: Running multiple epochs per sample takes hours or days
- Resource waste: Running unnecessary trials after statistical confidence is achieved
Optimal stopping helps you stop evaluating intelligently when you have enough data, potentially saving substantial API costs and evaluation time, whilst maintaining statistical validity and reliability.
# Install optstop with inspect_ai support (from GitHub - not yet on PyPI)
pip install "optstop[inspect] @ git+https://github.com/UKGovernmentBEIS/optstop.git"
# Or with GPU acceleration
pip install "optstop[inspect,gpu] @ git+https://github.com/UKGovernmentBEIS/optstop.git"from inspect_ai import Task, eval
from inspect_ai.dataset import Sample
from inspect_ai.scorer import model_graded_fact
from inspect_ai.solver import generate, system_message
from optstop.early_stopping import OptimalStoppingManager
# Configure optimal stopping
optstop_params = {
'delta_item': 0.05, # CI width threshold for individual samples
'delta_cap': 0.05, # CI width threshold for overall task
'cred_level': 0.97, # 97% credible intervals
'conservatism': 5, # Conservative for rare events
}
stopping_manager = OptimalStoppingManager(
optstop_params=optstop_params,
grouping_columns=['model', 'task'], # Group by model and task
reanalysis_interval=10, # Re-run inference every 10 samples
min_samples_per_grouping=5 # Minimum before first analysis
)
# Define your task with early_stopping attached
# IMPORTANT: early_stopping must be on Task, NOT passed to eval()
task = Task(
dataset=[Sample(input=q, target=a, id=f"q_{i}")
for i, (q, a) in enumerate(questions)],
solver=[
system_message("You are a helpful assistant."),
generate()
],
scorer=model_graded_fact(),
early_stopping=stopping_manager, # Attach to Task, NOT eval()
epochs=10 # Plan 10 epochs per sample
)
# Run evaluation
logs = eval(task, model="openai/gpt-4")
# Check efficiency gains (eval returns a list)
for log in logs:
if log.results.early_stopping:
diagnostics = log.results.early_stopping.metadata
print(f"Efficiency: {diagnostics['efficiency_percent']}%")
print(f"Stopped samples: {diagnostics['stopped_samples_count']}")Control how evaluations are grouped for stopping decisions:
# Group by model only (stop when model performance converges)
grouping_columns=['model']
# Group by model and task (separate stopping per model-task combination)
grouping_columns=['model', 'task']
# Group by custom metadata (e.g., difficulty level)
grouping_columns=['model', 'metadata.difficulty']
# Group by tags
grouping_columns=['model', 'tag.category']inspect_ai delivers scores as a two-level structure: an outer dict keyed by scorer name, where each entry contains a Score object whose .value is typically a scalar but can also be a dict when the scorer returns structured results.
# scores = {
# "accuracy": SampleScore(score=Score(value=1.0)), # scalar value
# "healthbench": SampleScore(score=Score(value={"healthbench_score": 0.72, "criteria_met": 5}))
# } # dict valuescore_choice selects which scorer to use (outer dict key). score_value_key extracts a specific field from a dict-valued Score.value (inner dict key). They address different levels and can be combined.
# Multiple scorers with scalar values: pick one by scorer name
OptimalStoppingManager(
optstop_params=params,
grouping_columns=['model'],
score_choice='accuracy' # Selects scores["accuracy"]
)
# Multiple scorers with scalar values: aggregate all
OptimalStoppingManager(
optstop_params=params,
grouping_columns=['model'],
score_agg='mean' # Averages all scorers (supports: mean, median, mode, max)
)
# Scorer returns a dict as Score.value (e.g., HealthBench):
# extract the numeric field by key
OptimalStoppingManager(
optstop_params=params,
grouping_columns=['model', 'task'],
score_value_key='healthbench_score', # Extracts value["healthbench_score"]
score_agg='mean',
)For tasks with ordinal ratings (e.g., 0-5 scale, 0-10 scale):
OptimalStoppingManager(
optstop_params=params,
grouping_columns=['model', 'task'],
ordinal_tasks=['confidence', 'rating'], # Tasks with ordinal scores
ordinal_max_score=10, # 0-10 scale (scores: 0,1,2,...,10)
ordinal_inference='hybrid' # Recommended: handles all distribution shapes
)Note: Ordinal scores are expected to be 0-indexed (range [0, ordinal_max_score]). If your scorer produces 1-indexed scores (e.g., 1-5 star ratings), transform them to 0-indexed before use, or open an issue on the GitHub repository.
Performance Note for Ordinal Discrete Tasks: Ordinal discrete inference (without score aggregation) uses entropy-based Bayesian models that are more computationally intensive than binary or continuous pathways (~2-3 minutes per inference call vs ~10-30 seconds, for typical 5-11 category scales with ~100 items). However, inference runs in a background thread that overlaps with LLM processing, so in practice ordinal inference adds no observable delay to overall evaluation time (in comparative testing, ordinal and continuous evaluations completed in virtually identical wall time). The more important difference is convergence behaviour: ordinal's entropy validation gate typically requires more data to converge, resulting in lower efficiency than binary or continuous pathways at the same precision threshold. GPU acceleration may help for evaluations with very fast-completing trials or large ordinal scales (20+ categories). See GPU Acceleration for setup details.
# Enable GPU for ordinal evaluations
manager = OptimalStoppingManager(
optstop_params=params,
grouping_columns=['model', 'task'],
ordinal_tasks=['rating'],
ordinal_inference='hybrid',
gpu_ids=[0] # GPU-accelerated MCMC sampling
)Compare performance with and without early stopping:
# Run with shadow mode (runs all trials but tracks what *would* stop)
manager = OptimalStoppi
10BC8
ngManager(
optstop_params=params,
grouping_columns=['model'],
shadow_mode=True # Disables actual stopping
)
# After evaluation, check what would have stopped
logs = eval(task, model="gpt-4")
for log in logs:
if log.results.early_stopping:
summary = log.results.early_stopping.metadata.get('shadow_mode_summary', {})
print(f"Would have stopped at trial: {summary.get('would_have_stopped_at')}")
print(f"Potential savings: {summary.get('potential_efficiency_percent')}%")| Parameter | Default | Description |
|---|---|---|
optstop_params |
Required | Dictionary of stopping parameters (delta_item, delta_cap, etc.) |
grouping_columns |
Required | List of columns for grouping decisions |
reanalysis_interval |
10 | Run inference every N completed samples |
min_samples_per_grouping |
5 | Minimum samples before first analysis |
ordinal_tasks |
None | List of task names using ordinal scoring |
ordinal_max_score |
10 | Maximum score for ordinal tasks |
ordinal_inference |
See note | Ordinal inference mode: 'modal', 'entropy', 'hybrid' |
ordinal_model_type |
'ordered_logistic' | Hierarchical model: 'ordered_logistic' or 'dirichlet' |
gpu_ids |
None | List of GPU IDs to use (e.g., [0, 1]) |
entropy_threshold |
0.8 | Proportion of max entropy for false peak detection in hybrid mode |
shadow_mode |
False | If True, run all trials but track stopping decisions |
score_choice |
None | Select scorer by name from the outer scores dict |
score_value_key |
None | Extract a field from dict-valued Score.value (inner dict key) |
score_agg |
None | Aggregate scores: 'mean', 'median', 'mode', 'max' |
random_seed |
None | Random seed for reproducibility (auto-generates if not specified) |
prior_mu |
0.0 | Centre of group-level Normal prior on logit scale (0.0 = 50% probability) |
prior_sigma |
None | Scale of group-level Normal prior. If None, uses pathway-specific defaults (binary/continuous: 1.5, ordinal: 2.0) |
Using informed priors: If you have recent results for the same model on the same benchmark, setting prior_mu to the logit of that known performance (e.g., scipy.special.logit(0.75) ≈ 1.1 for 75% accuracy) improves the accuracy of early point estimates and credible interval placement. Because credible intervals are computed on the probability scale via the logistic transform, a well-placed posterior also produces narrower probability-scale intervals than one centred near 0.5 at the same level of precision - which may modestly accelerate stopping for groupings whose true performance is far from 50%. When prior_sigma is not specified (None), pathway-specific defaults apply: 1.5 for binary/continuous, 2.0 for ordinal. If you explicitly set prior_sigma, that value applies to all pathways. The default values ensure that a mis-specified prior is overridden by data within approximately 15-20 items.
Note on ordinal_inference defaults:
- inspect_ai bridge (
OptimalStoppingManager): Defaults to'hybrid'- prioritises safety in automated evaluation contexts where stopping decisions have real cost implications. - Standalone functions (
optimal_stopping_posthoc,optimal_stopping_live, CLI): Defaults to'modal'- prioritises speed for interactive/exploratory analysis where users can iterate quickly.
Both modes are valid; choose based on your use case. Use 'hybrid' when accuracy is critical, 'modal' when speed matters more.
Additional optstop_params options:
| Key | Default | Description |
|---|---|---|
entropy_convergence_threshold |
0.10 | Absolute entropy CI width threshold on [0,1] scale for ordinal Pathway 2 convergence |
After evaluation, complete_task() returns comprehensive diagnostics:
# Assuming task was created with early_stopping=manager attached
logs = eval(task, model="gpt-4")
# eval() returns a list of logs
for log in logs:
if log.results.early_stopping:
diagnostics = log.results.early_stopping.metadata
print(f"Total planned trials: {diagnostics['total_planned_trials']}")
print(f"Trials run: {diagnostics['total_ran']}")
print(f"Trials skipped: {diagnostics['total_skipped']}")
print(f"Efficiency: {diagnostics['efficiency_percent']}%")
# Per-grouping breakdown
for grouping, metrics in diagnostics['decision_counters'].items():
print(f"{grouping}: {metrics['completed_samples']} samples completed")
# Stopped samples with reasons
for sample in diagnostics['stopped_samples']:
print(f"Sample {sample['id']}: {sample['reason']}")
# Convergence projections for non-stopped groupings
for grouping, history in diagnostics['stabilization_histories'].items():
if 'convergence_projection' in history:
proj = history['convergence_projection']
print(f"{grouping}: ~{proj['simple_proj_additional_trials']:.0f} more trials (1/√n estimate)")
print(f" Proximity: {proj['proximity_ratio']:.1f}x away from target")
print(f" Trajectory: {proj['trajectory_signal'] or 'n/a'}")
print(f" Exponential estimate: ~{proj['projected_additional_trials']} trials (order-sensitive)")
print(f" 80% CI: {proj['uncertainty']['ci_trials_80']} trials")For groupings that have not converged when evaluation ends, optstop estimates how many additional trials would be needed. This is available in both the bridge pathway (complete_task() diagnostics) and optimal_stopping_posthoc (per-grouping convergence_projection field in the summary list). The projection uses an exponential decay model as the primary approach (fitting w(t) = a*exp(-b*t) + c to the CI width trajectory), with linear extrapolation as a fallback when the exponential fit is unavailable or poor.
The projection classifies each non-converged grouping into one of three outcomes via convergence_target:
projected_width: CI width is projected to drop belowdelta_cap(the group-level CI width threshold, default 0.05) - the grouping would converge with more data. Increase your sample budget or epochs accordingly.projected_slope_stabilisation: CI width is projected to plateau abovedelta_cap- additional data yields diminishing returns. Widendelta_capto accept the current precision, or investigate whether the grouping has high intrinsic variance.projected_capped: Neither outcome detected within the projection horizon. Check whether the grouping has very few observations (< 5 group-level checks) - more data may clarify the trajectory. If observations are plentiful but no clear trend emerges, the data may be too noisy for the current stopping criteria.
Both the bridge pathway and optimal_stopping_posthoc include full uncertainty quantification via residual bootstrap (80% and 50% confidence intervals) and a confidence_level ('high', 'moderate', or 'low'). In convergence_posthoc, shortfall estimates are point estimates only (bootstrap disabled for speed, since results are aggregated across randomised orderings). Groupings with insufficient CI width history for projection will have convergence_projection set to None.
See the Convergence Projection Fields section in BRIDGE_API_REFERENCE.md for the full field reference (applicable to all modes, not just inspect_ai integration).
- Start conservative: Use
delta_item=0.05,delta_cap=0.05for high precision - Test with shadow mode: Run once with
shadow_mode=Trueto see potential savings - Set appropriate groupings: More granular groupings = more targeted stopping
- Monitor efficiency: If efficiency is 0%, your stopping criteria may be too strict
- Check compatibility: Verify inspect_ai version with
optstop.check_inspect_ai_compatibility("0.3.5") - Sample ID Randomisation: Ensure evals are constructed such that order of sample IDs is randomised (i.e., avoid systematic influences of sample ID order on potential performance)
- optstop version: 0.5.0
- inspect_ai version: 0.3.0+
- Python version: 3.10+
Check compatibility programmatically:
import optstop
print(f"optstop version: {optstop.__version__}")
print(f"Minimum inspect_ai version: {optstop.__min_inspect_ai_version__}")
# Check specific version
if optstop.check_inspect_ai_compatibility("0.3.5"):
print("Compatible!")- API reference: See
BRIDGE_API_REFERENCE.md- Complete parameter documentation and configuration guide
Issue: ImportError: No module named 'inspect_ai'
- Solution: Install the inspect extra:
pip install "optstop[inspect] @ git+https://github.com/UKGovernmentBEIS/optstop.git"
Issue: Early stopping not triggering
- Solution: Check
reanalysis_intervalandmin_samples_per_groupingsettings. Increase epochs if needed.
Issue: Invalid score warnings
- Solution: Verify scores are in valid range (0-1 for binary, 0-ordinal_max_score for ordinal)
Issue: GPU not detected
- Solution: Install the GPU extra:
pip install "optstop[gpu] @ git+https://github.com/UKGovernmentBEIS/optstop.git"
For more issues, see the development roadmap or open a GitHub issue.
optstop supports GPU acceleration for PyMC sampling operations, which speeds up MCMC sampling for large datasets and complex models.
- NVIDIA GPU with CUDA support
- JAX with GPU support installed:
pip install -U "jax[cuda12_pip]" -f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html
The package automatically detects GPU availability and configures optimal sampling parameters:
- GPU Available: Uses JAX/numpyro sampler with optimised chain/core settings
- CPU Only: Falls back to standard PyMC sampling
You can override automatic detection:
# Force GPU usage:
params = {'use_gpu': True, ...}
# Disable GPU acceleration:
params = {'use_gpu': False, ...}# Force GPU usage (will fail if GPU unavailable)
optstop-posthoc --csv data.csv --output pruned.csv --force_gpu [other options]
# Disable GPU even if available
optstop-posthoc --csv data.csv --output pruned.csv --disable_gpu [other options]- Faster MCMC sampling on typical workloads
- Automatic optimisation of chain/core parameters for GPU
Check your log file for GPU detection and usage information:
=== GPU Status Report ===
GPU Available: True
JAX Backend: gpu
GPU Device Count: 1
GPU Devices: ['cuda:0']
PyMC will use GPU acceleration via JAX/numpyro
========================
optstop supports both binary scoring (0/1) and ordinal scoring (e.g., 0-10) for both post-hoc and live optimal stopping modes.
- Binary scoring: Traditional 0/1 success/failure data (default)
- Ordinal scoring: Likert scale data (e.g., confidence ratings, difficulty ratings on 0-10 scale)
- Continuous bounded scoring: Pre-aggregated mean scores in [0, 1] range (e.g., average accuracy across sub-items), using a hierarchical Beta model with group-level parameters
- Mixed datasets: Seamlessly handle binary, ordinal, and continuous groupings in the same analysis
| Parameter | Type | Default | Description |
|---|---|---|---|
ordinal_tasks |
List[str] or None | None | Substrings to identify ordinal groupings (e.g., ['confidence', 'rating']) |
ordinal_max_score |
int | 10 | Maximum score for ordinal data (e.g., 10 for 0-10 scale) |
ordinal_inference |
str | 'modal' | Inference method: 'modal', 'entropy', or 'hybrid' (recommended) |
ordinal_model_type |
str | 'ordered_logistic' | Hierarchical model type: 'ordered_logistic' or 'dirichlet' |
entropy_threshold |
float | 0.8 | Proportion of max entropy for false peak detection in hybrid mode |
continuous_tasks |
List[str] or None | None | Substrings to identify continuous bounded groupings (e.g., ['mean_score', 'avg_rating']) |
Unusual scoring configurations? If your evaluation involves very large rubric scales (e.g., >20 categories), peculiar response distributions (e.g., models can only produce scores of 1, 3, 8, and 10 on a 0-10 scale), or you are uncertain about how to select or balance multiple scorer priorities for applying optimal stopping, please open an issue on the GitHub repository for guidance on configuring optstop for your use case.
Ordered Logistic (Recommended, Default):
- Cumulative link model that respects ordinal structure
- Uses identified cutpoints (first cutpoint fixed at 0 for model identification)
- Adaptive priors that scale with number of categories
- Faster than Dirichlet-Multinomial due to fewer parameters
- Best for truly ordinal data where category ordering matters
Dirichlet-Multinomial:
- Treats categories as exchangeable (no ordinal structure enforced)
- More robust to bimodal or U-shaped distributions
- Automatically used as fallback if ordered_logistic sampling fails
- Use when response distributions don't follow ordinal assumptions
Modal Mode (Fast):
- Bootstrap-based modal category estimation
- Suitable for clearly peaked ordinal distributions
- Typical processing time: ~0.1 seconds per call
Entropy Mode (Conservative):
- OrderedLogistic Bayesian model with full distribution entropy
- Suitable for diffuse ordinal distributions
- Typical processing time: ~2-3 minutes per call (5-11 category scales, ~100 items)
Hybrid Mode (RECOMMENDED):
- Pathway 1: Modal CI narrow + entropy validation (peaked data) → Fast stopping
- Pathway 2: Entropy convergence (diffuse data) → Safe stopping
- Prevents false peaks via
entropy_threshold - Typical processing time: ~2-3 minutes per call (same as entropy, since hybrid runs both modal and entropy components)
- Inference runs in a background thread, overlapping with LLM processing
All inference modes run in a background thread when used via OptimalStoppingManager, overlapping with LLM processing. The timings above reflect inference computation, not evaluation delays. See the Performance Note above for details.
import pandas as pd
from optstop import optimal_stopping_posthoc
# Mixed binary/ordinal dataset
df = pd.DataFrame({
'student': [1]*20 + [2]*20,
'task': ['math']*10 + ['confidence']*10 + ['math']*10 + ['confidence']*10,
'item_id': list(range(5))*8,
'trial_num': [1,2]*20,
'score': [1,0,1,1,0] * 4 + [7,8,9,6,8] * 4 # Binary + ordinal
})
params = {
'delta_item': 0.15,
'delta_cap': 0.05,
'cred_level': 0.97,
'conservatism': 5,
}
pruned_df, summary = optimal_stopping_posthoc(
df, params,
grouping_columns=['student', 'task'],
sample_id_column='item_id',
epoch_column='trial_num',
score_column='score',
# Ordinal parameters:
ordinal_tasks=['confidence', 'difficulty', 'rating'], # Substring matching
ordinal_max_score=10, # 0-10 scale
ordinal_inference='hybrid', # RECOMMENDED
ordinal_model_type='ordered_logistic', # Cumulative link model (default)
entropy_threshold=0.8 # False peak detection
)import pandas as pd
from optstop import optimal_stopping_live
# Current data snapshot
df = pd.DataFrame({
'student': [1]*10,
'task': ['math']*5 + ['confidence']*5,
'item_id': list(range(5))*2,
'trial_num': [1]*10,
'score': [1,0,1,1,0] + [7,8,9,6,8]
})
params = {
'delta_item': 0.15,
'delta_cap': 0.05,
'cred_level': 0.97,
}
result = optimal_stopping_live(
df, params,
grouping_columns=['student', 'task'],
sample_id_column='item_id',
epoch_column='trial_num',
score_column='score',
# Ordinal parameters:
ordinal_tasks=['confidence'], # Identify ordinal groupings
ordinal_max_score=10, # 0-10 scale
ordinal_inference='hybrid', # RECOMMENDED
ordinal_model_type='ordered_logistic', # Cumulative link model (default)
entropy_threshold=0.8 # False peak detection
)
print(result['stop_sample_ids']) # Item IDs that reached stopping criteria
print(result['stop_task']) # Groupings that reached stopping criteriaFor tasks with pre-aggregated mean scores in the [0, 1] range (e.g., average accuracy across multiple sub-items):
import pandas as pd
from optstop import optimal_stopping_posthoc
# Dataset with continuous bounded scores (pre-aggregated means)
df = pd.DataFrame({
'model': ['gpt-4']*20 + ['claude']*20,
'task': ['qa_accuracy']*10 + ['summary_quality']*10 + ['qa_accuracy']*10 + ['summary_quality']*10,
'item_id': list(range(5))*8,
'epoch': [1,2]*20,
'score': [0.85, 0.82, 0.91, 0.78, 0.88] * 4 + [0.72, 0.75, 0.68, 0.81, 0.77] * 4 # Mean scores in [0,1]
})
params = {
'delta_item': 0.10,
'delta_cap': 0.05,
'cred_level': 0.97,
}
pruned_df, summary = optimal_stopping_posthoc(
df, params,
grouping_columns=['model', 'task'],
sample_id_column='item_id',
epoch_column='epoch',
score_column='score',
# Continuous bounded parameters:
continuous_tasks=['accuracy', 'quality'], # Substrings to identify continuous groupings
)Choosing between ordinal and continuous pathways:
Integer-valued scores (Likert scales, rubric scores, quality ratings) can be routed to either the ordinal or continuous bounded pathway. The right choice depends on your data and scoring design:
- Aggregated scores (e.g., mean of multiple sub-rubric criteria via
score_agg='mean'): Use the continuous bounded pathway. Aggregation produces effectively continuous values on a bounded interval, making this a natural fit. - Raw discrete scores with well-populated categories (all categories observed, items per grouping >= 5x the number of categories): The ordinal pathway (
ordinal_tasks) preserves rank structure without imposing interval-scale assumptions, and is the more principled choice when these conditions hold. - Raw discrete scores with sparse categories (some categories rarely or never observed): The ordinal pathway can produce biased estimates and miscalibrated credible intervals under category sparsity (empirically observed: CI coverage as low as 25% at 97% nominal level for ordered logistic; 70% for Dirichlet-Multinomial). In this situation, consider whether scores can be aggregated (e.g., mean across sub-criteria) to route through the continuous pathway, or whether the evaluation design can be adjusted to ensure better category coverage. Routing raw sparse scores through the continuous pathway via
continuous_tasksis possible but imposes interval-scale assumptions that may not be warranted for all ordinal data.
The package logs a warning when category sparsity is detected under ordinal inference.
Ordinal estimand note: The ordinal pathway estimates the modal category (most probable score), not the mean score. The group-level theta reported for ordinal groupings is modal_category / max_score, and credible intervals bracket the mode. This differs from binary and continuous pathways, which estimate mean performance. When comparing theta values across pathways, be aware that ordinal theta reflects categorical concentration rather than average score.
Binary Model Details:
The binary inference pathway uses a logit-normal hierarchical model (mu_group ~ Normal(prior_mu, sigma) on the logit scale, where sigma defaults to 1.5 when prior_sigma is None). A Beta-Binomial alternative was evaluated through controlled simulation under conditions favouring the Beta-Binomial (Beta-Binomial data generating process with κ=10, 94% HDI credible intervals). In the mid-range (0.1-0.9 true performance), the two models are statistically indistinguishable in bias, credible interval width, coverage, and stopping behaviour (absolute bias 0.022 vs 0.022, CI width 0.070 vs 0.070). At exact boundaries (0.0 or 1.0), point-estimate bias is marginally lower for the Beta-Binomial (0.007 vs 0.005) but credible interval widths are near-identical (0.011 vs 0.011), and both models show zero coverage - an expected consequence of the degenerate boundary data-generating process, not a model deficiency. Because equivalent CI widths imply equivalent stopping times, the model choice does not alter stopping behaviour where evaluations typically operate. The decisive difference is computational reliability: the Beta-Binomial produced 120× more MCMC divergences than the logit-normal in the mid-range, rising to 9,206× at exact boundaries (with effective sample sizes as low as 7), indicating a poorly conditioned posterior geometry in precisely the sparse, boundary-adjacent regime an adaptive stopping framework must handle robustly.
Continuous Bounded Model Details: The continuous bounded pathway uses a hierarchical Beta model with the following structure:
- mu_group: Group-level mean (logit scale)
- sigma_group: Between-item standard deviation (captures item-level variability)
- phi_group: Group-level precision parameter
- mu_item: Item-level means (hierarchical, derived from mu_group + z * sigma_group)
This hierarchical structure correctly accounts for between-item variance when computing group-level credible intervals.
-
Automatic Detection: The package uses substring matching to identify score types
ordinal_tasks=['confidence']: Any grouping containing "confidence" (case-insensitive) uses ordinal scoringcontinuous_tasks=['accuracy']: Any grouping containing "accuracy" (case-insensitive) uses continuous bounded scoring- All other groupings use binary scoring (default)
-
Score Validation:
- Ordinal scores are validated to be in range [0,
ordinal_max_score] - Continuous scores are validated to be in range [0, 1]
- Binary scores are validated to be 0 or 1
- Ordinal scores are validated to be in range [0,
-
Independent Processing: Each grouping is processed with the correct scoring method (no cross-contamination)
-
False Peak Detection: In hybrid mode for ordinal scoring,
entropy_thresholdprevents premature stopping on diffuse data
Expected Group Accuracy via mean(Theta)
The package computes group-level credible intervals using the expected group accuracy: mean(Theta) across all items, rather than using individual item Theta values or sigmoid(mu_group).
Why this matters:
- In hierarchical models,
Theta_i = sigmoid(mu_group + z_i * sigma_group)for each item - The expected group accuracy
E[Theta]is the mean across items, NOTsigmoid(mu_group) - When
sigma_groupis large,sigmoid(mu_group)can be very different fromE[Theta]- Example:
mu_group=5.3,sigma_group=5.3givessigmoid(mu_group)=0.995butE[Theta]=0.83
- Example:
Previous approaches and their limitations:
Theta[0](first item only): Arbitrary, depends on data orderingsigmoid(mu_group): Measures "typical item" (z=0), not expected accuracymean(Theta): Correctly computes expected group accuracy ✓
This methodology correctly accounts for between-item variance (sigma_group) when computing credible intervals, providing more accurate stopping decisions for hierarchical data structures.
| Function | Binary | Ordinal | Continuous Bounded | Notes |
|---|---|---|---|---|
optimal_stopping_posthoc |
Yes | Yes | Yes | All inference modes available |
optimal_stopping_live |
Yes | Yes | Yes | All inference modes available |
convergence_posthoc |
Yes | Yes | Yes | All inference modes available |
# Post-hoc mode with ordinal scoring
optstop-posthoc --csv data.csv --output pruned.csv \
--grouping_columns student,task \
--sample_id_column item_id \
--epoch_column trial_num \
--score_column score \
--ordinal_tasks confidence,rating \
--ordinal_max_score 10 \
--ordinal_inference hybrid \
--entropy_threshold 0.8
# Live mode with continuous bounded scoring
optstop-live --csv current_data.csv \
--grouping_columns model,task \
--sample_id_column item_id \
--epoch_column trial_num \
--score_column score \
--continuous_tasks accuracy,quality
# Mixed: ordinal + continuous in same dataset
optstop-posthoc --csv mixed_data.csv --output pruned.csv \
--grouping_columns model,task \
--sample_id_column item_id \
--epoch_column trial_num \
--score_column score \
--ordinal_tasks confidence \
--ordinal_max_score 10 \
--continuous_tasks accuracypip install .Note: Basic installation includes all core functionality. Ordinal inference will work but use the slower PyMC default sampler instead of the numpyro backend from the performance extras.
For optimal performance, especially with ordinal scoring, install with performance extras:
pip install .[performance]This installs:
- JAX (CPU backend): Enables faster ordinal inference
- numpyro (JAX-based sampler): Handles OrderedLogistic models efficiently
Performance impact:
- Binary inference: No difference (uses PyMC default for speed)
- Continuous inference: No difference (uses PyMC default for speed)
- Ordinal inference: faster with numpyro compared to PyMC default
For using optstop with inspect_ai evaluations:
pip install .[inspect]
# Or with performance extras (recommended for ordinal tasks):
pip install .[inspect,performance]For GPU acceleration (requires NVIDIA GPU with CUDA support):
pip install .[gpu]See GPU Acceleration for detailed setup instructions, manual JAX installation, and performance benefits.
Install multiple extras at once:
# Recommended: performance + inspect_ai
pip install .[inspect,performance]
# Full GPU setup
pip install .[gpu]
# All features including development tools
pip install .[inspect,performance,dev]For contributing to optstop:
pip install .[dev]This includes testing tools (pytest, pytest-asyncio), code formatting (black), linting (flake8), and type checking (mypy).
For a reproducible environment, install from the pinned lockfile, then the dev extras:
pip install -r requirements-lock.txt
pip install -e .[dev]Run the suite per test file so each file's tests execute in a clean process:
for f in tests/test_*.py; do python -m pytest "$f" -m "not slow"; doneThis is the recommended way to run the full suite. The tests build many PyMC models in one interpreter, and PyMC tracks active model contexts in process-global state; across the large number of MCMC-based tests that state can accumulate and produce spurious No model on context stack errors late in a single-process run. Every test file passes on its own and the package itself is unaffected - running per file simply keeps that process-global state clean. To isolate a single test while debugging, pytest-forked is available (python -m pytest --forked tests/<file>::<test>). Slow ordinal-MCMC tests are marked slow and skipped by -m "not slow"; drop that flag to include them.
| Installation | Binary | Continuous | Ordinal | Use Case |
|---|---|---|---|---|
pip install . |
✓ Fast (PyMC default) | ✓ Fast (PyMC default) | ⚠ Slower (PyMC default) | Basic usage, no ordinal tasks |
pip install .[performance] |
✓ Fast (PyMC default) | ✓ Fast (PyMC default) | ✓ Fast (numpyro) | Recommended for ordinal tasks |
pip install .[gpu] |
✓ Fastest (numpyro GPU) | ✓ Fastest (numpyro GPU) | ✓ Fastest (numpyro GPU) | GPU hardware available |
To log all optimal stopping decisions and details to a file (and optionally to console), call configure_optstop_logging before running your analysis:
from optstop.rule import configure_optstop_logging
# Log to file only (default - suppresses console output)
configure_optstop_logging()
# Or specify a custom log file
configure_optstop_logging('my_optstop_log.txt', console_output=False)
# To also log to console (shows INFO messages in terminal)
configure_optstop_logging('my_optstop_log.txt', console_output=True)After running, you can open the log file to review all stopping decisions and details. Look for entries such as:
Starting live optimal stoppingStopping sample_id ...orStopping task/grouping ...Live optimal stopping complete
The log file will contain all stopping decisions and the reasons for them, making it easy to audit or debug the stopping process.
You can control the behaviour of the optimal stopping algorithms through optimal_stopping_posthoc and optimal_stopping_live. Settings are supplied through two channels: a params dictionary and direct keyword arguments. Any omitted setting uses its default value.
Which channel? Task routing and model selection (
ordinal_tasks,continuous_tasks,ordinal_max_score,ordinal_inference,ordinal_model_type,entropy_threshold,prior_mu,prior_sigma), along withprocessing_order,reanalysis_interval,shuffle_items,shuffle_seed,score_column,display_progress,generate_diagnostics, anddiagnostics_prefix, are keyword arguments - pass them directly in the function call (as the examples do), not insideparams. A value placed inparamsfor one of these is silently ignored; for the routing options that means the grouping is scored with the default binary pathway, with no warning. The CI thresholds, conservatism, and MCMC settings in the table below are read from theparamsdict.
| Parameter | Default | Applies to | Description |
|---|---|---|---|
grouping_columns |
required | Both | List of column names (or single column) to use for grouping |
sample_id_column |
required | Both | Column name for sample ID |
epoch_column |
required | Both | Column name for epoch/trial |
score_column |
'score' | Both | Column name for score (default: 'score') |
display_progress |
True | Both | Show a progress bar for groupings (set False to disable) |
generate_diagnostics |
False | Post-hoc | Generate diagnostic plots comparing full vs pruned datasets |
diagnostics_prefix |
"optstop_diagnostics" | Post-hoc | Prefix for diagnostic output files (PNG and CSV) |
delta_item |
0.05 | Both | Max acceptable CI width for individual items |
delta_cap |
0.05 | Both | Max acceptable CI width for task/grouping |
cred_level |
0.97 | Both | Credibility level for intervals (e.g., 0.97 for 97% CI) |
conservatism |
5 | Both | Factor for rare event conservatism (higher = more conservative) |
low_performance_threshold |
0.01 | Both | Below this rate: suppress automatic stops (all pathways); ordinal groupings additionally report an inflated (floored) CI width |
Parameter interaction - conservatism and low_performance_threshold (ordinal): For ordinal groupings, when estimated performance falls below low_performance_threshold the effective CI-width target tightens to delta_cap / conservatism. With defaults (conservatism=5, delta_cap=0.05), this target is 0.01 - achievable with moderate data. If you raise low_performance_threshold, more groupings enter this conservative regime; if you also keep conservatism high, the tightened target may become unreachable within your data budget. When adjusting either parameter, check that delta_cap / conservatism remains achievable for the sample sizes you expect. (For binary and continuous groupings the below-threshold behaviour is suppression rather than a tightened target - see "Detecting low base-rate capabilities" below.)
| draws | 1000 | Both | Number of MCMC samples for PyMC (affects speed/accuracy) |
| tune | 1000 | Both | Number of tuning steps for PyMC |
| chains | 4 | Both | Number of MCMC chains for PyMC |
| cores | 4 | Both | Number of CPU cores for PyMC sampling |
| rep_batch_size | 1 | Both | Number of repetitions to process in each batch |
| pymc_refresh_every | 2 | Both | How often to run the PyMC model (every N items) |
| stab_window | 15 | Both | Window size for assessing CI stabilisation |
| CI_delta | 0.00001 | Both | Slope threshold for determining CI stabilisation |
| target_accept | 0.95 | Both | NUTS sampler target acceptance rate (higher = fewer divergences, slower) |
| use_gpu | Auto | Both | Enable/disable GPU acceleration (True/False, auto-detected if not set) |
| force_gpu | False | Both | Force GPU usage, fail if unavailable (for CLI --force_gpu) |
| ordinal_tasks | None | All | List of substrings to identify ordinal groupings (e.g., ['confidence']) |
| ordinal_max_score | 10 | All | Maximum score for ordinal data (e.g., 10 for 0-10 scale) |
| ordinal_inference | 'modal' | All | Inference method: 'modal', 'entropy', or 'hybrid' |
| ordinal_model_type | 'ordered_logistic' | All | Hierarchical model: 'ordered_logistic' (default) or 'dirichlet' |
| entropy_threshold | 0.8 | All | Proportion of max entropy for false peak detection in hybrid mode |
| entropy_convergence_threshold | 0.10 | All | Absolute entropy CI width on [0,1] scale for ordinal Pathway 2 convergence |
| prior_mu | 0.0 | All | Centre of group-level Normal prior on logit scale (0.0 = 50% probability) |
| prior_sigma | None | All | Scale of group-level Normal prior. None→pathway defaults (binary/cont: 1.5, ordinal: 2.0)|
| continuous_tasks | None | All | List of substrings to identify continuous bounded [0,1] groupings |
| shuffle_items | False | Post-hoc | Randomise item order within each grouping before processing (recommended to avoid selection bias from sorted input) |
| shuffle_seed | None | Post-hoc | Random seed for reproducible shuffling (only used with shuffle_items=True) |
| processing_order | 'item_greedy' | Post-hoc | Processing order: 'item_greedy' (all epochs per item, fast) or 'epoch_interleaved' (all items per epoch, matches production bridge behaviour) |
| reanalysis_interval | 10 | Post-hoc | Trials between group model refreshes in epoch_interleaved mode (matches bridge default) |
You can set any combination of the params-dict rows above. For example:
params = {
'delta_item': 0.05, # Require a narrower CI for stopping items
'delta_cap': 0.05, # Task/grouping CI width threshold
'cred_level': 0.97, # Use 97% credible intervals
'conservatism': 5, # More conservative for rare events
'low_performance_threshold': 0.01,# Adjust threshold for low performance
'draws': 1000, # MCMC samples
'tune': 1000, # Tuning steps
'chains': 4, # Number of MCMC chains
'cores': 4, # Number of CPU cores for sampling
'rep_batch_size': 2, # (post-hoc only) Process 2 reps at a time
'pymc_refresh_every': 1, # (post-hoc only) Run PyMC every item
'stab_window': 15, # Stabilisation window size
'CI_delta': 0.00001, # Stabilisation threshold
}Note:
- Both live and post-hoc modes support the same core parameters for optimal stopping logic.
- Live mode does not support diagnostic plot generation (
generate_diagnostics,diagnostics_prefix). - You can set only the parameters you care about; the rest will use defaults.
- By default, a progress bar is shown for groupings. Set
display_progress=Falseto disable it in Python, or use--no_progressin the CLI.
When using optimal_stopping_posthoc, you can generate comprehensive diagnostic plots comparing your full dataset with the pruned dataset. This helps you understand the efficiency and accuracy of the optimal stopping algorithm.
Set generate_diagnostics=True in your function call:
pruned_df, summary = optimal_stopping_posthoc(
df, params,
grouping_columns=['subject', 'task'],
sample_id_column='item_id',
epoch_column='trial_num',
generate_diagnostics=True,
diagnostics_prefix='my_analysis_diagnostics'
)The diagnostic system generates two files:
- PNG file (
{prefix}.png): A comprehensive 5-panel diagnostic plot - CSV file (
{prefix}_paired.csv): Paired data for custom analysis
The diagnostic plot includes:
- Full vs. Pruned Scatter Plot: Shows how well the pruned estimates match the full dataset estimates
- Bland-Altman Plot: Displays the agreement between full and pruned estimates
- Efficiency Distribution: Histogram of epochs saved per task
- Sample IDs Saved by Grouping: Bar chart showing percentage of sample IDs saved for each grouping
- Credible Intervals Comparison: Side-by-side comparison of credible intervals from full vs. pruned datasets
The system also logs comprehensive statistics including:
- Accuracy metrics: Bias, MAE, RMSE, Pearson correlation, ICC(3,1)
- Efficiency metrics: Mean epochs saved, percentage of total epochs saved, items saved
You can also enable diagnostics via the CLI:
optstop-posthoc --csv mydata.csv --output pruned.csv --grouping_columns subject,task --sample_id_column item_id --epoch_column trial_num --score_column accuracy --generate_diagnostics --diagnostics_prefix my_diagnosticsAll main functions now support flexible column mapping. You must specify:
grouping_columns: List of column names (or a single column name) to use for grouping (e.g.,['subject', 'task']or'subject')sample_id_column: Name of the column for sample ID (e.g.,'item_id')epoch_column: Name of the column for epoch/trial (e.g.,'trial_num')score_column: Name of the column for score (e.g.,'score')
The functions will internally create the necessary numeric columns for grouping, sample ID, and epoch.
See Quick Start for a complete post-hoc example with data formatting, column mapping, and output interpretation. If you only have one grouping column, you can pass 'subject' or ['subject'].
For standalone live analysis, see the optimal_stopping_live example below. For live stopping via inspect_ai, see Using optstop with inspect_ai.
The optimal_stopping_live function returns a dictionary with two key outputs:
- Format:
"grouping_sample_id"(e.g.,"1-1_2"for grouping1-1, sample ID2) - Purpose: Sample IDs that have reached stopping criteria and no longer need additional epochs
- Usage: Use these to stop data collection for specific sample IDs within their groupings
- Example:
['1-1_1', '1-1_2', '2-1_3']means sample IDs 1 and 2 in grouping 1-1, and sample ID 3 in grouping 2-1 have reached stopping criteria
- Format: Grouping names (e.g.,
"1-1","2-1") - Purpose: Groupings that have reached stopping criteria and no longer need additional sample IDs
- Usage: Use these to stop data collection for entire groupings
- Example:
['1-1', '2-1']means both groupings 1-1 and 2-1 have reached stopping criteria
- The function processes each unique grouping combination independently
- Each grouping can reach stopping criteria independently of others
- Sample IDs are prefixed with their grouping to avoid confusion across multiple groupings
- If no stopping criteria are met:
{'stop_sample_ids': [], 'stop_task': []} - Continue data collection for all sample IDs and groupings
result = optimal_stopping_live(df, params, grouping_columns=['subject', 'task'], ...)
# Check for sample IDs to stop
if result['stop_sample_ids']:
print("Stop collecting epochs for these sample IDs:")
for sample_id in result['stop_sample_ids']:
grouping, original_id = sample_id.split('_', 1)
print(f" - Sample ID {original_id} in grouping {grouping}")
# Check for groupings to stop
if result['stop_task']:
print("Stop collecting sample IDs for these groupings:")
for grouping in result['stop_task']:
print(f" - Grouping {grouping}")
# If both lists are empty, continue data collection
if not result['stop_sample_ids'] and not result['stop_task']:
print("Continue data collection - no stopping criteria met")import pandas as pd
from optstop.rule import configure_optstop_logging
from optstop
10BC8
span> import convergence_posthoc
configure_optstop_logging('my_convergence_log.txt', console_output=False)
# Mixed binary/ordinal dataset
df = pd.DataFrame({
'student': [1]*20 + [2]*20,
'task': ['math']*10 + ['confidence']*10 + ['math']*10 + ['confidence']*10,
'item_id': list(range(5))*8,
'trial_num': [1,2]*20,
'score': [1,0,1,1,0] * 4 + [7,8,9,6,8] * 4 # Binary + ordinal
})
params = {
'delta_item': 0.05,
'delta_cap': 0.05,
'draws': 1000,
'tune': 1000,
'chains': 4,
'cores': 4,
'stab_window': 15,
'CI_delta': 0.00001,
'rep_batch_size': 1,
'pymc_refresh_every': 2,
'item_seqs': 3, # Randomisation sequences
'epoch_seqs': 3 # Randomisation sequences
}
result = convergence_posthoc(
df, params,
grouping_columns=['student', 'task'],
sample_id_column='item_id',
epoch_column='trial_num',
score_column='score',
# Ordinal parameters:
ordinal_tasks=['confidence'], # Identify ordinal groupings
ordinal_max_score=10, # 0-10 scale
ordinal_inference='hybrid', # RECOMMENDED (fastest with optimizations)
entropy_threshold=0.8 # False peak detection
)
print(result)The package provides functions for adaptive optimal stopping, allowing you to determine when enough data has been collected for reliable inference, either retrospectively (post-hoc) or during live data collection.
- The
optimal_stopping_posthocfunction now parallelises across groupings (unique combinations of the columns you specify for grouping), using all available CPU cores for efficient processing of large datasets. - Each grouping-task is processed independently and in parallel, with results aggregated at the end.
- The
optimal_stopping_livefunction processes each unique grouping combination independently - Parallel Processing: Uses adaptive parallel processing across groupings, similar to post-hoc mode
- Returns sample IDs with grouping prefixes (e.g.,
"1-1_2") to clearly identify which sample IDs belong to which groupings - Returns a list of grouping names that have reached stopping criteria, allowing you to stop entire groupings
- Supports multiple groupings in a single function call, with independent stopping decisions per grouping
- Progress Tracking: Optional progress bar display (controlled by
display_progressparameter)
For detailed best practices including parameter settings, grouping strategies, and reproducibility guidance, see Best Practices & Recommendations.
See Quick Start - Step 1 for column roles, example data layout, and a worked example. Additional requirements:
- Long format: Each row is one observation (one model's response to one item on one epoch). Do not pass wide-format or pivoted data.
- Complete epoch labelling: Every observation needs a valid epoch value. If your data has only one epoch per sample, the sample-level stopping criteria will have limited data to work with.
- Consistent score ranges: All samples within an ordinal grouping should share the same score scale. Set
ordinal_max_scoreto match your rubric maximum (e.g., 10 for a 0-10 scale, 5 for a 1-5 scale). - Sample ordering: For post-hoc analysis, samples are processed in order of their first appearance in the DataFrame. If your data is sorted by some systematic property (e.g., item difficulty), consider shuffling before analysis to satisfy the exchangeability assumption (see
shuffle_itemsin the Quick Start). - Additional columns are preserved in the output but are not used by the algorithm.
These requirements apply to both optimal_stopping_posthoc (batch analysis of completed data) and optimal_stopping_live (standalone incremental analysis). For live stopping via inspect_ai, see the bridge integration guide and BRIDGE_API_REFERENCE.md - data formatting is handled automatically by the framework.
The standalone optimal_stopping_live function analyses a DataFrame incrementally, simulating the stopping decisions that would have occurred during sequential data collection. This is distinct from the inspect_ai bridge, which integrates directly with live evaluation pipelines.
from optstop import optimal_stopping_live
result = optimal_stopping_live(
df, params={},
grouping_columns=['model', 'task'],
sample_id_column='item_id',
epoch_column='epoch',
score_column='score'
)The package provides a function for post-hoc convergence analysis, allowing you to assess when statistical stability would have been achieved in sequential testing scenarios. This is useful for determining the minimum data required for reliable performance estimates.
- Purpose:
- Runs a post-hoc convergence analysis on a full dataset, parallelising across groupings (using all available CPU cores).
- Returns a DataFrame detailing the required numbers of trials at epoch and sample_ID levels, split by the user's desired grouping.
- Parameters:
- Accepts the same
paramsdictionary as other functions (see table above), plus:item_seqs: Number of randomised item orderings per grouping (default: 20)epoch_seqs: Number of randomised epoch orderings per item (default: 20)
- Accepts the same
- Returns:
- A DataFrame with one row per grouping-task, containing detailed convergence statistics and summary metrics. Key columns include:
mean_needed_items/var_needed_items: Mean and population variance of projected additional items needed across randomised orderingsmean_needed_epochs/var_needed_epochs: Mean and population variance of projected additional epochs neededtheta_ci_width: Final group-level CI widthpercent_items_used: Fraction of total trials used before convergencemean_fin_CI_width_item/mean_fin_CI_width_epoch: Mean final CI widths at item and epoch level
- Shortfall estimates (needed items/epochs) are computed using an exponential decay model (primary, fitting
w(t) = a*exp(-b*t) + c) with linear extrapolation fallback, projecting item-level convergence againstdelta_item. Projections are capped at 200 additional trials per sequence. Bootstrap uncertainty is disabled for posthoc shortfalls (point estimates only, aggregated across randomised orderings).
- A DataFrame with one row per grouping-task, containing detailed convergence statistics and summary metrics. Key columns include:
import pandas as pd
from optstop.rule import configure_optstop_logging
from optstop import convergence_posthoc
configure_optstop_logging('my_convergence_log.txt', console_output=False)
# Mixed binary/ordinal dataset
df = pd.DataFrame({
'subject': [1]*8 + [2]*8,
'grouping': ['A-math']*4 + ['A-confidence']*4 + ['B-math']*4 + ['B-confidence']*4,
'task': ['math']*4 + ['confidence']*4 + ['math']*4 + ['confidence']*4,
'item_id': [1,1,2,2]*4,
'trial_num': [1,2,1,2]*4,
'score': [1,0,1,1] + [7,8,9,6] + [0,1,1,0] + [8,9,7,8], # Binary + ordinal
})
params = {
'delta_item': 0.05,
'delta_cap': 0.05,
'draws': 500,
'tune': 500,
'stab_window': 5,
'CI_delta': 0.0002,
'rep_batch_size': 1,
'pymc_refresh_every': 2,
'item_seqs': 3, # Recommended: 20 for production
'epoch_seqs': 3 # Recommended: 20 for production
}
result = convergence_posthoc(
df, params,
grouping_columns=['subject', 'task'],
sample_id_column='item_id',
epoch_column='trial_num',
score_column='score',
# Ordinal parameters:
ordinal_tasks=['confidence'], # Identify ordinal groupings
ordinal_max_score=10, # 0-10 scale
ordinal_inference='hybrid', # RECOMMENDED (fastest with optimizations)
entropy_threshold=0.8 # False peak detection
)
print(result)The resulting DataFrame contains all the convergence metrics for each grouping-task. The analysis is parallelised for speed.
When you run convergence_posthoc, the package now automatically generates two diagnostic figures by default:
- {prefix}_grouped_needed.png: Two vertically stacked plots showing mean needed items and mean needed epochs (with std error bars) by grouping (descending order).
- {prefix}_score_scatter.png: Scatter plots of mean needed items/epochs vs. performance, as before.
You can control this behaviour:
- Python: Pass
generate_diagnostics=Falsetoconvergence_posthocto turn off diagnostics, or setdiagnostics_prefixto change the output file prefix. - CLI: Use
--no_diagnosticsto turn off diagnostics, and--diagnostics_prefixto set the output file prefix (default:convergence_eval).
result = convergence_posthoc(
df, params,
grouping_columns=['subject', 'task'],
sample_id_column='item_id',
epoch_column='trial_num',
generate_diagnostics=True, # default is True
diagnostics_prefix='my_convergence_eval'
)optstop-convergence --csv mydata.csv --output convergence_stats.csv --grouping_columns subject,task --sample_id_column item_id --epoch_column trial_num --score_column accuracy --no_diagnostics --diagnostics_prefix my_convergence_eval- By default, diagnostics are ON. Use
--no_diagnosticsto disable. - The output files will be named
my_convergence_eval_grouped_needed.pngandmy_convergence_eval_score_scatter.png.
See the CLI help (optstop-convergence --help) for all options.
- Required columns: Your input DataFrame must include the columns you specify for grouping, sample ID, epoch, and score (see Flexible Column Mapping section above).
- Group labels: If you want human-readable group labels in your output, include a
groupingcolumn (e.g., system/model name) in addition to your grouping columns. - Parallelisation: Each unique combination of the columns you specify for grouping will be processed in parallel, so ensure these columns are set appropriately for your experimental design.
- Items are processed in dataframe order (within each grouping). If your input data is sorted (e.g., alphabetically by sample ID), early stopping may select a biased subsample if early-alphabet items happen to have systematically different scores than late-alphabet items.
- Use
shuffle_items=Trueto randomise item order within each grouping before processing. This is recommended for unbiased post-hoc analysis. Inepoch_interleavedmode, shuffling controls the order items are processed within each epoch. - Use
shuffle_seedwithshuffle_items=Truefor reproducible shuffling. - The package warns automatically if sorted input is detected and
shuffle_items=False, but using explicit shuffling is recommended.
The processing_order parameter controls how the posthoc function iterates through the data:
'item_greedy'(default): Processes all epochs for item 1, then all epochs for item 2, etc. This is fast but produces deep coverage of few items early on. Suitable for exploratory analysis and continuous scoring where each observation carries high information.'epoch_interleaved': Processes all items for epoch 1, then all items for epoch 2, etc. This matches the production bridge (OptimalStoppingManager) processing order, producing broad item coverage early. Recommended for consistency/robustness studies comparing posthoc results against production, particularly for binary scoring where item diversity is critical.
The reanalysis_interval parameter (default 10) controls how frequently the group-level model is refreshed in epoch_interleaved mode, matching the bridge's default cadence. It is ignored in item_greedy mode (which uses pymc_refresh_every instead).
# Epoch-interleaved mode (matches production bridge behaviour)
pruned_df, summary = optimal_stopping_posthoc(
df, params,
grouping_columns=['model', 'task'],
sample_id_column='item_id',
epoch_column='trial_num',
processing_order='epoch_interleaved',
reanalysis_interval=10,
shuffle_items=True,
shuffle_seed=42
)- draws & tune: 1000 (default) is sufficient for standard analysis. Increase
tuneup to 2000 if you see many divergences, poor adaptation messages, or R-hat far from 1.0. Increasedrawsup to 4000 if HDIs lack sufficient precision. Values below 1000 (e.g., 50) are only for debugging. - chains & cores: Default values of
chains=4andcores=4are suitable for most analyses. Increasechainsfor more robust MCMC sampling andcoresfor faster parallel sampling (up to your system's CPU core count). - CI width thresholds:
delta_item: 0.05 is a common choice for high precision; 0.1 is more lenient.delta_cap: 0.05 for group-level precision; increase for faster but less precise stopping.
- Stabilisation parameter:
CI_deltacontrols how stable the CI slope must be before stopping. Smaller values (e.g., 0.0002) require more stability; larger values allow earlier stopping.
- Conservatism:
conservatism=5is typical; increase for more caution in low-performance scenarios.
- Low base-rate capabilities:
- To detect a rare capability and stop once it is observed, set
low_performance_thresholdto the success rate of interest. See "Detecting low base-rate capabilities" below.
- To detect a rare capability and stop once it is observed, set
- Reproducibility:
- For reproducible pruned DataFrames, set a random seed before running your analysis (e.g.,
np.random.seed(42)or passrandom_seedin params). - Note: Due to the stochastic nature of MCMC and parallelisation, summary statistics (e.g., CI bounds, widths) are not guaranteed to be bitwise reproducible, even with the same random seed. Only the pruned DataFrame is guaranteed to be reproducible; summary values may differ slightly between runs.
- For reproducible pruned DataFrames, set a random seed before running your analysis (e.g.,
- Logging:
- Use
configure_optstop_logging()to log all stopping decisions to file (console output is suppressed by default).
- Use
params = {
'delta_item': 0.05, # High precision for items
'delta_cap': 0.05, # High precision for group/task
'draws': 1000, # Default (increase up to 4000 if HDIs lack precision)
'tune': 1000, # Default (increase up to 2000 if many divergences or R-hat issues)
'chains': 4, # Number of MCMC chains
'cores': 4, # Number of CPU cores for sampling
'CI_delta': 0.00001, # Require stable CI slope
'conservatism': 5, # Typical value
# ... other parameters as needed ...
}See the parameter table above for all options and defaults.
The current defaults are optimised for safety - strict stopping criteria and minimum viable inference burden. This means the algorithm will be conservative in its stopping decisions, preferring to collect more data rather than risk premature stopping.
- To stop more aggressively, relax decision criteria: widen
delta_itemanddelta_cap(CI width thresholds for samples and groupings respectively), increaseCI_delta(stabilisation slope threshold, where higher values allow earlier stopping), and lowercred_leveltowards 0.9. - If you see many divergences, poor adaptation messages, or R-hat far from 1.0, increase
tune(up to 2000). - If HDIs lack sufficient precision, increase
draws(up to 4000).
The credible intervals produced by optstop are well-calibrated when groupings contain 50+ items with performance in the 0.2-0.8 range. For smaller samples or near-boundary performance (close to 0% or 100%), CIs may undercover due to hierarchical shrinkage - a known property of Bayesian hierarchical models.
Factors affecting coverage:
- Sample size: Coverage approaches nominal levels (~97%) as grouping size increases toward 50+ items
- Performance range: Mid-range performance (0.2-0.8) yields better-calibrated intervals than boundary cases
- Hierarchical shrinkage: Small groupings with extreme performance (near 0% or 100%) may exhibit substantial undercoverage
In stress-test conditions (e.g., 18-30 items per grouping with many boundary performers), observed coverage can fall substantially below nominal. Under more typical conditions with adequate sample sizes and mid-range performance, coverage is considerably better. The package automatically detects near-boundary estimates and emits diagnostic warnings recommending that raw proportions be reported alongside model estimates.
Recommendations for small samples or boundary performance:
- Treat CIs as rough guides rather than formal statistical intervals
- Report raw proportions alongside model estimates for transparency
- Run multiple shuffled orderings (use
shuffle_items=Truewith different seeds) to assess estimate stability - For formal inference, consider collecting more data or using methods designed for small samples
This limitation affects both live and post-hoc modes equally, as it is structural to hierarchical Bayesian models rather than a bug in the implementation.
Some evaluations target a capability that is expected to be very rare - for example a hazardous behaviour that should occur at most once in a few thousand trials. Two properties of the package matter here.
The resolution floor. Group-level inference clamps the likelihood logit to
±6, so the likelihood cannot distinguish rates below sigmoid(-6) ≈ 0.0025
(about 1 in 400). Since version 0.5.0 the reported credible interval is read
from an unclipped transform, so its lower bound can approach 0 and the interval
can contain a near-zero truth. However, below the resolution floor the interval
is prior-dominated: it is a qualitative indication that the rate is very small,
not a calibrated bound. When the whole reported interval lies below the floor,
the result carries pinned=True; report the raw observed proportion alongside
it. (Continuous scores have a second, independent variance clamp in the
likelihood, so sub-1% continuous intervals are doubly heuristic.)
Using low_performance_threshold as a detection trigger. Set
low_performance_threshold to the success rate of interest (e.g. 0.0005 for
1 in 2,000). While the cumulative observed rate stays below the threshold you
have not yet seen the event, so for both the binary and continuous
pathways the automatic precision and slope-stabilisation stops are suppressed at
every level - item, sample, and grouping - and sampling continues (at the
grouping level this is surfaced as low_perf_stop_suppressed=True). This holds
even if the accumulated null data would otherwise satisfy the width or slope
criterion - the gate keeps the search open until the event actually appears.
When the rare event occurs it lifts the cumulative observed rate to or above the threshold, which releases the hold. The ordinary precision stop can then fire once the interval is narrow enough. In effect you sample until you have seen roughly one event's worth of evidence at your rate of interest, then stop.
Two caveats:
- The trigger is the cumulative observed rate crossing the threshold, which is
stochastic. The first event is expected near
1/ptrials, so a late first event may need a second before the running rate clears the bar, and an early one clears it immediately. - Set the threshold to the rate of interest, not above it (a higher threshold oversamples, waiting for more events) or below it (releases the hold before you have the evidence).
If your rate of interest is below the resolution floor (~0.25%), the event still
lifts the observed rate and releases the hold, but the reported bound remains
qualitative (pinned=True): you have confirmed and halted on the capability, not
precisely quantified how rare it is.
The ordinal pathway. The pinned reliability caveat is specific to the
binary and continuous pathways, which map a logit through a sigmoid that is
clamped at ±6 - the source of the resolution floor. The ordinal estimator has
no such location clamp, so its low-performance estimates are data-faithful and
carry no equivalent reliability caveat. For symmetry, an ordinal grouping that
resolves at a very low normalised performance level (below
low_performance_threshold) still reports a telemetry flag, low_perf_floor=True,
so low-capability ordinal groupings are as easy to detect as low-rate
binary/continuous ones. Unlike pinned, it is keyed off the normalised
performance point estimate (so it is independent of the modal/entropy inference
mode) and is not a reliability warning - the ordinal estimate below it is still
trustworthy. It is telemetry only and does not affect stopping. Group-level stop
suppression via low_performance_threshold applies to all three pathways
(surfaced as low_perf_stop_suppressed=True); the item-level suppression
described above is binary/continuous only, because on homogeneous near-zero data
the ordinal item-level interval is held open by a separate minimum-width floor
rather than by the low-performance gate.
Log visibility. Suppression is silent by default in the returned results
beyond the low_perf_stop_suppressed field. To make it visible while a run is in
progress, the first time a stop is suppressed for a grouping optstop emits a
single INFO-level log on the relevant optstop.* logger naming the grouping and
quoting the resolved low_performance_threshold value (so it reflects any value
you passed, not the default). Enable it with
configure_optstop_logging(console_output=True) (its default level is already
INFO) or by attaching your own handler to the optstop logger. Per-check detail
(each individual suppressed width/slope stop) is available at DEBUG.
MIT
Q: I get a ValueError about missing columns.
- Make sure your DataFrame includes all required columns that you specify for grouping, sample ID, epoch, and score.
Q: My summary statistics are not exactly reproducible, even with the same random seed.
- This is expected due to the stochastic nature of MCMC and parallelisation. Only the pruned DataFrame is guaranteed to be reproducible; summary values may differ slightly between runs.
Q: I get PyMC or sampling errors (e.g., "Too few samples", "NUTS initialisation failed").
- Increase
tuneup to 2000 if you see many divergences or R-hat issues, anddrawsup to 4000 if HDIs lack precision. For very small test runs, warnings are expected. - Ensure your data is not empty or all-NaN for any grouping.
Q: The code is slow or uses a lot of CPU.
- The package parallelises across groupings. If you have many groupings, this can use all available CPU cores. You can reduce the number of groupings or run on a machine with more resources.
Q: How do I get more detailed logs?
- Use
configure_optstop_logging()to log to file (console output is suppressed by default). Check the log file for detailed stopping decisions and errors.
Q: How do I interpret the 'error' field in the output?
- If a grouping fails (e.g., due to bad data), the 'error' field will contain the error message. All other fields for that grouping will be None.
The package provides CLI entry points for all major functions. After installing with pip install ., you can use the following commands from your terminal:
Command:
optstop-posthoc --csv mydata.csv --output pruned.csv --summary summary.csv --grouping_columns subject,task --sample_id_column item_id --epoch_column trial_num --score_column accuracy --delta_item 0.05 --delta_cap 0.05 --draws 1000 --tune 1000 --chains 4 --cores 4 --CI_delta 0.00001 --conservatism 5 --rep_batch_size 1 --pymc_refresh_every 2 --stab_window 15 --random_seed 42 --generate_diagnostics --diagnostics_prefix my_diagnostics
- --csv: Path to input CSV file (required)
- --output: Path to output pruned CSV file (required)
- --summary: Path to output summary CSV file (optional)
- --grouping_columns: Comma-separated list or single column name for grouping (required)
- --sample_id_column: Column name for sample ID (required)
- --epoch_column: Column name for epoch/trial (required)
- --score_column: Column name for score (default: score)
- --log: Path to log file (default: optstop_cli.log)
- --no_progress: Disable the progress bar (shown by default)
- --generate_diagnostics: Generate diagnostic plots comparing full vs pruned datasets (optional)
- --diagnostics_prefix: Prefix for diagnostic output files (default: optstop_diagnostics)
- --delta_item: Max acceptable CI width for individual items (default: 0.05)
- --delta_cap: Max acceptable CI width for task/grouping (default: 0.05)
- --draws: Number of MCMC samples for PyMC (default: 1000)
- --tune: Number of tuning steps for PyMC (default: 1000)
- --chains: Number of MCMC chains for PyMC (default: 4)
- --cores: Number of CPU cores for PyMC (default: 4)
- --CI_delta: Slope threshold for determining CI stabilisation (default: 0.00001)
- --conservatism: Factor for rare event conservatism (default: 5)
- --rep_batch_size: Number of repetitions to process in each batch (default: 1)
- --pymc_refresh_every: How often to run the PyMC model (default: 2)
- --stab_window: Window size for assessing CI stabilisation (default: 15)
- --random_seed: Random seed for reproducible results (optional)
- --low_performance_threshold: Success rate below which automatic stops are suppressed to avoid a false stop on an unresolved low base rate; ordinal groupings additionally report an inflated (floored) CI width (default: 0.01)
- --disable_gpu: Disable GPU acceleration even if available
- --force_gpu: Force GPU usage (will fail if GPU unavailable)
- --ordinal_tasks: Comma-separated list of substrings to identify ordinal groupings (e.g., "confidence,rating")
- --ordinal_max_score: Maximum score for ordinal data (default: 10)
- --ordinal_inference: Ordinal inference method: modal, entropy, or hybrid (default: modal)
- --ordinal_model_type: Hierarchical model type: ordered_logist 9999 ic or dirichlet (default: ordered_logistic)
- --entropy_threshold: Proportion of max entropy for false peak detection in hybrid mode (default: 0.8)
- --prior_mu: Centre of group-level Normal prior on logit scale (default: 0.0 = 50% probability)
- --prior_sigma: Scale of group-level Normal prior. If not set, uses pathway defaults (binary/cont: 1.5, ordinal: 2.0)
- --shuffle_items: Randomise item order within each grouping before processing. Recommended to avoid selection bias from sorted input.
- --shuffle_seed: Random seed for reproducible shuffling (only used with --shuffle_items)
- --processing_order: Processing order:
item_greedy(all epochs per item, fast) orepoch_interleaved(all items per epoch, matches production). Default:item_greedy - --reanalysis_interval: Group model refresh interval in trials for
epoch_interleavedmode (default: 10) - --continuous_tasks: Comma-separated list of substrings to identify continuous bounded [0,1] groupings (e.g., "accuracy,quality")
Command:
optstop-live --csv current_data.csv --grouping_columns subject --sample_id_column item_id --epoch_column trial_num --score_column accuracy --delta_item 0.05 --delta_cap 0.05 --draws 1000 --tune 1000 --chains 4 --cores 4 --CI_delta 0.00001 --conservatism 5 --rep_batch_size 1 --pymc_refresh_every 2 --stab_window 15 --random_seed 42
- --csv: Path to input CSV file (required)
- --grouping_columns: Comma-separated list or single column name for grouping (required)
- --sample_id_column: Column name for sample ID (required)
- --epoch_column: Column name for epoch/trial (required)
- --score_column: Column name for score (default: score)
- --log: Path to log file (default: optstop_live.log)
- --no_progress: Disable the progress bar (shown by default)
- --delta_item: Max acceptable CI width for individual items (default: 0.05)
- --delta_cap: Max acceptable CI width for task/grouping (default: 0.05)
- --draws: Number of MCMC samples for PyMC (default: 1000)
- --tune: Number of tuning steps for PyMC (default: 1000)
- --chains: Number of MCMC chains for PyMC (default: 4)
- --cores: Number of CPU cores for PyMC (default: 4)
- --CI_delta: Slope threshold for determining CI stabilisation (default: 0.00001)
- --conservatism: Factor for rare event conservatism (default: 5)
- --rep_batch_size: Number of repetitions to process in each batch (default: 1)
- --pymc_refresh_every: How often to run the PyMC model (default: 2)
- --stab_window: Window size for assessing CI stabilisation (default: 15)
- --random_seed: Random seed for reproducible results (optional)
- --low_performance_threshold: Success rate below which automatic stops are suppressed to avoid a false stop on an unresolved low base rate; ordinal groupings additionally report an inflated (floored) CI width (default: 0.01)
- --disable_gpu: Disable GPU acceleration even if available
- --force_gpu: Force GPU usage (will fail if GPU unavailable)
- --ordinal_tasks: Comma-separated list of substrings to identify ordinal groupings (e.g., "confidence,rating")
- --ordinal_max_score: Maximum score for ordinal data (default: 10)
- --ordinal_inference: Ordinal inference method: modal, entropy, or hybrid (default: modal)
- --ordinal_model_type: Hierarchical model type: ordered_logistic or dirichlet (default: ordered_logistic)
- --entropy_threshold: Proportion of max entropy for false peak detection in hybrid mode (default: 0.8)
- --prior_mu: Centre of group-level Normal prior on logit scale (default: 0.0 = 50% probability)
- --prior_sigma: Scale of group-level Normal prior. If not set, uses pathway defaults (binary/cont: 1.5, ordinal: 2.0)
- --continuous_tasks: Comma-separated list of substrings to identify continuous bounded [0,1] groupings (e.g., "accuracy,quality")
- Prints which sample IDs (with grouping prefix) and/or groupings can be stopped.
Command:
optstop-convergence --csv mydata.csv --output convergence_stats.csv --grouping_columns subject,task --sample_id_column item_id --epoch_column trial_num --score_column accuracy --delta_item 0.05 --delta_cap 0.05 --draws 1000 --tune 1000 --chains 4 --cores 4 --CI_delta 0.00001 --conservatism 5 --rep_batch_size 1 --pymc_refresh_every 2 --stab_window 15 --item_seqs 20 --epoch_seqs 20 --random_seed 42 --no_diagnostics --diagnostics_prefix my_convergence_eval
- --csv: Path to input CSV file (required)
- --output: Path to output convergence stats CSV file (required)
- --grouping_columns: Comma-separated list or single column name for grouping (required)
- --sample_id_column: Column name for sample ID (required)
- --epoch_column: Column name for epoch/trial (required)
- --score_column: Column name for score (default: score)
- --log: Path to log file (default: optstop_convergence.log)
- --no_progress: Disable the progress bar (shown by default)
- --no_diagnostics: Disable diagnostic plots (default: ON)
- --diagnostics_prefix: Prefix for diagnostic output files (default: convergence_eval)
- --delta_item: Max acceptable CI width for individual items (default: 0.05)
- --delta_cap: Max acceptable CI width for task/grouping (default: 0.05)
- --draws: Number of MCMC samples for PyMC (default: 1000)
- --tune: Number of tuning steps for PyMC (default: 1000)
- --chains: Number of MCMC chains for PyMC (default: 4)
- --cores: Number of CPU cores for PyMC (default: 4)
- --CI_delta: Slope threshold for determining CI stabilisation (default: 0.00001)
- --conservatism: Factor for rare event conservatism (default: 5)
- --rep_batch_size: Number of repetitions to process in each batch (default: 1)
- --pymc_refresh_every: How often to run the PyMC model (default: 2)
- --stab_window: Window size for assessing CI stabilisation (default: 15)
- --item_seqs: Number of randomised item orderings per grouping (default: 20)
- --epoch_seqs: Number of randomised epoch orderings per item (default: 20)
- --random_seed: Random seed for reproducible results (optional)
- --low_performance_threshold: Success rate below which automatic stops are suppressed to avoid a false stop on an unresolved low base rate; ordinal groupings additionally report an inflated (floored) CI width (default: 0.01)
- --ordinal_tasks: Comma-separated list of substrings to identify ordinal groupings (e.g., "confidence,rating")
- --ordinal_max_score: Maximum score for ordinal data (default: 10)
- --ordinal_inference: Ordinal inference method: modal, entropy, or hybrid (default: modal)
- --ordinal_model_type: Hierarchical model type: ordered_logistic or dirichlet (default: ordered_logistic)
- --entropy_threshold: Proportion of max entropy for false peak detection in hybrid mode (default: 0.8)
- --prior_mu: Centre of group-level Normal prior on logit scale (default: 0.0 = 50% probability)
- --prior_sigma: Scale of group-level Normal prior. If not set, uses pathway defaults (binary/cont: 1.5, ordinal: 2.0)
- --continuous_tasks: Comma-separated list of substrings to identify continuous bounded [0,1] groupings (e.g., "accuracy,quality")
- --disable_gpu: Disable GPU acceleration even if available
- --force_gpu: Force GPU usage (will fail if GPU unavailable)
For any command, you can see all options and help text with:
optstop-posthoc --help
optstop-live --help
optstop-convergence --help
- Progress bar is shown by default. Use
--no_progressto disable it for silent or script-based runs. - Always check your input CSV for required columns: Make sure the columns you specify for grouping, sample ID, and epoch exist in your data.
- Set a random seed (
--random_seed) for reproducible pruned DataFrames. - Consider using
--shuffle_itemsfor post-hoc analysis to avoid selection bias from sorted input. If your input data is sorted (e.g., alphabetically by sample ID), early stopping may select a biased subsample if early-alphabet items happen to have systematically different scores. The package will warn you if sorted input is detected, but using--shuffle_items(optionally with--shuffle_seedfor reproducibility) is recommended for unbiased results. - Use recommended parameter values for real analyses (see Best Practices above).
- Check the log file for detailed stopping decisions, errors, and parameter validation.
- For large datasets, run on a machine with sufficient CPU and memory.
- If you encounter errors, check the FAQ and log file for troubleshooting tips.
Note: After running any of the main functions (optimal_stopping_posthoc, optimal_stopping_live, or convergence_posthoc), you will see a message printed to the console reminding you where to find the log file with all details and warnings. This log file contains all stopping decisions, errors, and PyMC warnings, even if the terminal output is quiet.