Skip to content

Migration Guide

This guide covers migration between the released v1 API, the normative v2 release contract, and the v3 Polars tabular container contract, as well as the earlier 0.x-to-v1 transition. The API reference links each machine-readable contract. Experimental and provisional extensions do not receive the same compatibility guarantee.

Use the tested tabular translation reference for explicit ordering, alignment, missing-value and formatting differences.

v3 replaces xarray.Dataset with polars.DataFrame as the Python in-memory reference model for ValueArray and ParameterSet. The xarray surface (ValueArray.dataset, ParameterSet.dataset, from_dataset, and to_dataset) is removed outright at 3.0.0 in favour of ValueArray.frame, ParameterSet.frame, from_polars, and to_polars, with no compatibility extra, no deprecation window, and no shim. The frozen v1 and v2 contracts remain in specs/v1/ and specs/v2/; the v3 contract is in specs/v3/.

Removed xarray symbols and v3 replacements

Section titled “Removed xarray symbols and v3 replacements”
Removed in 3.0 v3 replacement Notes
ValueArray.dataset ValueArray.frame Wide polars.DataFrame with one Float64 column per strategy in declared order
ParameterSet.dataset ParameterSet.frame Wide polars.DataFrame with one Float64 column per parameter in declared order
ValueArray.from_dataset(ds) ValueArray.from_polars(frame) or ValueArray.from_numpy(values, strategy_names) Validates finite Float64 columns and rejects unrecognised extra variables
ParameterSet.from_dataset(ds) ParameterSet.from_polars(frame) or ParameterSet.from_numpy_or_dict(parameters) Validates finite Float64 columns and non-empty parameter frames
ValueArray.to_dataset() ValueArray.to_polars() or ValueArray.numpy_values Returns a cloned polars.DataFrame or a NumPy ndarray
ParameterSet.to_dataset() ParameterSet.to_polars() or ParameterSet.parameters Returns a cloned polars.DataFrame or dict[str, np.ndarray]

Container access and ValueArray.values return type

Section titled “Container access and ValueArray.values return type”

In v2, ValueArray.values returned an xarray.DataArray while ValueArray.numpy_values returned a numpy.ndarray. In v3, ValueArray.values returns a numpy.ndarray directly (matching numpy_values). Callers that accessed .frame or Polars expressions use va.frame or va.to_polars(); callers that wrote va.values.values should use va.numpy_values or va.values.

import numpy as np
import polars as pl
from voiage.schema import ParameterSet, ValueArray
va = ValueArray.from_polars(
pl.DataFrame({"Usual care": [1500.0, 1490.0], "Screening": [1625.0, 1650.0]})
)
frame = va.frame
matrix = va.numpy_values
ps = ParameterSet.from_polars(
pl.DataFrame({"cost": [120.0, 140.0], "effect": [0.02, 0.03]})
)
param_dict = ps.parameters

Unchanged constructors, multi-perspective arrays, and decision entry points

Section titled “Unchanged constructors, multi-perspective arrays, and decision entry points”

The array-first constructors and decision-analysis entry points are unchanged:

  • ValueArray.from_numpy, ValueArray.from_numpy_perspectives, ValueArray.numpy_values, ValueArray.strategy_names, and ValueArray.perspective_names keep their signatures and return types. Multi-perspective 3D ValueArray instances (from_numpy_perspectives) store one pl.Array(pl.Float64, n_perspectives) column per strategy alongside perspective_names and continue to return a 3D (n_samples, n_strategies, n_perspectives) array from numpy_values and values.
  • ParameterSet.from_numpy_or_dict, ParameterSet.parameters, and ParameterSet.parameter_names keep their signatures and return types.
  • DecisionAnalysis, evpi, evppi, evsi, normal_normal_two_arm_evsi, enbs, ceaf, and dominance are unchanged.
  • No new persistence format is introduced, and the JSON wire contract and Rust, R, and Julia binding surfaces are unchanged.

Transitive dependencies and external tabular ingestion

Section titled “Transitive dependencies and external tabular ingestion”

pandas and xarray are no longer installed transitively by voiage. Downstream code that imports pandas or xarray directly must declare those packages in its own dependencies.

External callers holding a pandas.DataFrame, polars.DataFrame, or PyArrow table can pass it through the backend-neutral dataframe ingestion route, which consumes __arrow_c_stream__ and __dataframe__ without voiage importing pandas:

import polars as pl
from voiage.ingestion import from_dataframe
from voiage.schema import ParameterSet, ValueArray
value_bundle = from_dataframe(external_df, dataset_id="net-benefits")
va = ValueArray.from_polars(pl.from_arrow(value_bundle.table("data")))
param_bundle = from_dataframe(external_parameters_df, dataset_id="parameters")
ps = ParameterSet.from_polars(pl.from_arrow(param_bundle.table("data")))

The ingestion adapter accepts caller-owned frames through the existing Arrow capsule or DataFrame-interchange protocols. The caller owns its producer installation; voiage does not import that library. Producer indexes remain excluded, with explicit bindings selecting the numerical columns.

On the qualified Polars 2.1 path, numeric Arrow buffers are shared through the capsule interface. The normal allow_copy=True diagnostic stays not_observable: a shared numeric buffer in this fixture is not a guarantee for every dtype or producer. allow_copy=False preserves the existing strict policy. PyArrow tables support it; the qualified pandas floating-point fixture requires a validity-mask conversion, and Polars 2.1 has no __dataframe__ fallback accepting that flag, so these frames retain the documented rejection.

Non-conforming producers raise ValueError with input does not satisfy the dataframe interchange protocol with the requested copy policy. The error retains its underlying cause. No alternate persistence format or numerical conversion is introduced by producer qualification.

v2 corrects the scientific model used by the built-in two-loop expected value of sample information (EVSI) calculation. The correction changes numerical behaviour, not only implementation details, so it is a major-version change. The immutable v1 contract remains in specs/v1/; the v2 contract is in specs/v2/.

The v1 signature ended with a positional-or-keyword seed and did not expose study-model callbacks:

evsi(
model_func,
psa_prior,
trial_design,
population=None,
discount_rate=None,
time_horizon=None,
method="two_loop",
n_outer_loops=100,
n_inner_loops=1000,
metamodel="linear",
seed=None,
)

In v2, seed becomes keyword-only, and the paired custom-model callbacks are also keyword-only:

evsi(
model_func,
psa_prior,
trial_design,
population=None,
discount_rate=None,
time_horizon=None,
method="two_loop",
n_outer_loops=100,
n_inner_loops=1000,
metamodel="linear",
*,
seed=None,
trial_simulator=None,
posterior_sampler=None,
)

Pass seed by name before upgrading:

value = evsi(
model,
prior,
design,
method="two_loop",
n_outer_loops=500,
n_inner_loops=2_000,
seed=20260724,
)

Do not compare a v1 and v2 result as if they were two implementations of the same estimator. Preserve the v1 environment, input artifact, seed, result, and version metadata when an analysis requires an audit trail.

Concern v1 v2
Built-in prior empirical PSA rows for outer truths; separate marginal normal updates one fitted joint multivariate-normal prior throughout
Current decision empirical PSA sample draws or integration from the fitted joint prior
Posterior observed arm means updated independently exact joint Gaussian conditioning for every correlated stochastic parameter
Fixed outcome SD part of the parameter sample validated as finite, constant, and positive; excluded from stochastic covariance
Inner-loop meaning repeated posterior/model calculations with v1 sample-size behaviour exactly n_inner_loops joint posterior draws per outer data set
Negative simulation estimate clipped to zero by the public calculation returned untruncated so simulation error remains visible
Custom study model unavailable paired keyword-only simulation and posterior callbacks
Unseeded RNG NumPy process-global stream fresh local numpy.random.Generator
Seeded RNG local generator, but seed could be positional local generator with keyword-only unsigned 64-bit seed

The coherent built-in model requires:

  • finite PSA rows from which a joint multivariate-normal prior can be fitted;
  • one unique mean_<arm> stochastic parameter for each study arm;
  • a common sd_outcome that is finite, strictly positive, and constant across the PSA rows;
  • positive, non-Boolean integer outer- and inner-loop counts; and
  • a covariance matrix that can be conditioned safely.

Inputs that do not define that model raise InputError or NumericalError. Raw NumPy linear-algebra exceptions are not part of the public contract.

Use normal_normal_two_arm_evsi when the decision problem has one uncertain incremental effect, an equal-allocation two-arm normal study with known common outcome variance, and linear incremental net benefit:

from voiage import normal_normal_two_arm_evsi
value = normal_normal_two_arm_evsi(
prior_mean=0.06,
prior_standard_deviation=0.03,
outcome_standard_deviation=1.0,
total_sample_size=200,
net_benefit_slope=50_000.0,
net_benefit_intercept=-3_000.0,
)

This function is analytical, Rust-owned, deterministic, and uses no random number generator. All arguments are keyword-only and total_sample_size is a positive even integer.

If the fitted-normal model is not appropriate, provide both callbacks:

value = evsi(
model,
prior,
design,
method="two_loop",
seed=20260724,
trial_simulator=simulate_trial,
posterior_sampler=sample_joint_posterior,
)

trial_simulator(true_joint_parameters, trial_design, rng) returns one simulated data set. posterior_sampler(prior, trial_data, trial_design, n_draws, rng) returns exactly n_draws complete, joint posterior rows in a ParameterSet. Both callbacks need to represent the same prior, likelihood, and posterior model.

The regression, efficient, efficient-regression, and moment-based EVSI estimators are provisional in v2. Treat them as compatibility tools rather than substitutes for a declared study model.

Python, R, and Julia packages that expose EVSI need to identify the contract version they implement. A binding remains on the v1 contract until its signature, local-RNG behaviour, model semantics, errors, fixtures, and documentation conform to v2. Merely linking a newer shared library does not establish v2 conformance.

v1 supports Python 3.12, 3.13, and 3.14. Install a platform wheel or build with the documented Rust toolchain. The Rust core is required for stable numerical operations: v1 does not silently fall back to an independent Python numerical implementation.

If the native module or a required Rust capability is unavailable, stable operations raise BackendNotAvailableError. Treat that as an installation or capability error; do not catch it and substitute an unverified 0.x algorithm.

Prefer the stable top-level symbols:

from voiage import DecisionAnalysis, ParameterSet, evpi, evppi, evsi

Replace raw dictionary parameter samples passed to evppi with a ParameterSet. The temporary dictionary bridge emits FutureWarning with diagnostic code deprecated_raw_dict_parameter_samples:

from voiage import ParameterSet, evppi
parameters = ParameterSet.from_numpy_or_dict(
{"treatment_effect": treatment_effect_samples}
)
result = evppi(net_benefit, parameters, ["treatment_effect"])

Stable results and errors are Rust-owned versioned contracts. Python may expose NumPy or xarray views for convenience, but callers should not depend on private Python implementation modules or experimental result shapes.

Go, TypeScript, .NET, WASM, and the duplicate standalone Rust binding are not retained v1 bindings. The supported repository-owned surfaces are Rust, Python/PyO3, R, and Julia. Mojo remains an external upstream interoperation boundary until an approved toolchain and distribution contract exist.

Web, widget, accelerator, distributed, and research methods are optional or experimental. Install their named extras deliberately and consult each page’s maturity label before relying on it in a production compatibility contract.

Test migration in a clean environment and retain the prior environment lock, input fixtures, random seeds, and result checksums until representative v1 analyses pass. Do not overwrite or move a signed release tag. If v1 exposes a material regression, preserve the failing evidence, open an issue with the smallest non-confidential reproducer, and pin the last known-good published version while a new patch release proceeds through the full release workflow.

Code using the legacy TreeAgeConnector, import_treeage_model, export_to_treeage, or convert_treeage_to_voi names may continue to run, but those APIs now explicitly describe a repository-authored .voiage.xml profile. Do not pass an arbitrary TreeAge export or describe the result as TreeAge compatible without separate round-trip evidence.

The R connector accepts BCEA- and hesim-inspired JSON profiles only. RDS and native R package objects are unsupported. Excel input and output is limited to .xlsx and requires the excel extra; .xls remains unsupported.

Binding consumers must also stop inferring method parity from the retained package list. DecisionProblem is public in Python, internal to the Rust domain crate, and unavailable through the C ABI, R, and Julia. Use the current method-level capability registry before selecting a binding.

The new value_of_equity_information method is fixture-backed and measures the value of resolving uncertain equity weights across policy-relevant subgroups. It is intentionally separate from the experimental value_of_distributional_equity method, which measures subgroup tailoring.

The new method reports baseline and resolved social-welfare decisions, scenario probabilities, subgroup expected net benefits, and a sample-allocation diagnostic. Cross-language parity and licensed open-data attribution remain promotion gates; no stable claim is made yet.

value_of_ambiguity_distribution_shift is also fixture-backed. It uses source-sample reweightings to model plausible distribution shift and reports a radius-penalized robust strategy, scenario regret, and drift sensitivity.

value_of_adaptive_learning_bandit adds fixture-backed sequential allocation with UCB, Thompson, and epsilon-greedy policies. It reports exploration cost, regret, switching, sampling burden, and stopping diagnostics; open-data attribution and cross-language parity remain stable-promotion gates.

value_of_federated_privacy_preserving adds site-local summary aggregation without centralizing individual-level records. It reports privacy-budget loss, secure-aggregation error, disclosure-risk metadata, and site contribution values, with synthetic-summary fallback when individual access is blocked. Licensed multi-site data, cross-language parity, and stable promotion remain explicit gates.

value_of_ai_assisted_evidence_triage adds fixture-backed, human-in-the-loop VOI for screening and extraction support. It reports sensitivity, specificity, decision-impact-weighted false exclusion/inclusion burden, reviewer time saved, audit value, extraction error, model drift, and automation economics. Licensed evidence-corpus validation, cross-language parity, and stable promotion remain explicit gates.

The fixture-backed value_of_interoperability_standardization method is available for harmonization, common-data-model reuse, and transformation-error analysis. It reports standardization cost separately and should remain experimental/fixture-backed until licensed cross-site evidence and parity review are complete.

The fixture-backed value_of_regulatory_market_access method models approval, reimbursement, label, pricing, coverage, delay, and evidence-package economics. It remains fixture-backed until calibrated regulatory and payer evidence plus cross-language parity are available.

The fixture-backed value_of_replication_reproducibility method is available through Python and the calculate-replication-reproducibility CLI command. It does not change existing method defaults and remains subject to explicit data provenance and parity gates.

The fixture-backed value_of_evidence_obsolescence_refresh method is available through Python and the calculate-evidence-obsolescence-refresh CLI command. It models refresh cadence without changing existing method defaults.

The fixture-backed value_of_strategic_behavior method is available through Python and the calculate-strategic-behavior CLI command. It remains subject to calibrated-game-data and parity gates.