Skip to content

Donor-aware workflow

Donor-aware inference separates population-level, between-donor, and within-donor variation. Use it only when donor identifiers are scientifically meaningful and approved for the analysis.

Simulate heterogeneous donors

from barracuda import DonorSimulationSpec, simulate_donor_event_counts

spec = DonorSimulationSpec(
    donor_sizes={"D1": 40, "D2": 55, "D3": 35},
    model_key="hetero3",
    mu_lambda={"D1": 3.0, "D2": 4.5, "D3": 5.0},
    sigma_lambda={"D1": 1.0, "D2": 1.4, "D3": 0.8},
    p_zero={"D1": 0.20, "D2": 0.10, "D3": 0.25},
    observation_time=1.0,
    seed=2026,
)
frame, truth = simulate_donor_event_counts(spec)

Each donor parameter may be a shared scalar, donor-labelled mapping, or sequence aligned to donor_sizes. Model-incompatible values are rejected—for example, homo requires zero dispersion and zero structural-zero probability.

Fit donor-aware models

from barracuda import InferenceSettings, run_donor_aware_models

settings = InferenceSettings(draws=256, chains=1, cores=1, seed=73)
fits = run_donor_aware_models(
    frame,
    observation_time=1.0,
    settings=settings,
    model_keys=["dis2p", "hetero3"],
)

The result mapping contains the same InferenceResult type as donor-ignorant fits, with donor_aware=True and ordered donor_labels.

Extract paired posterior draws

from barracuda import donor_posterior_frame, population_posterior_frame

fit = fits["hetero3"]
population = population_posterior_frame(fit, max_draws=5_000)
donors = donor_posterior_frame(fit, max_draws=5_000)

Rows retain chain and draw, so parameters from one posterior particle remain paired. Public frames use p_zero_population and p_zero_donor even when an older backend stores phi_0_*.

Variance decomposition

from barracuda import population_variance_decomposition

moments = population_variance_decomposition(
    fit,
    donor_weights=[40, 55, 35],
)

The xarray result contains within-donor, between-donor, and total rate variance for every posterior draw. In zero-inflated models, rate moments use active-cell weights proportional to w_d * (1 - p_zero_d).

Weights are normally observed donor cell counts. They are not inferred sample weights, and a different weighting target answers a different question.

Leave-one-donor-out sensitivity

from barracuda import leave_one_donor_out_moments

sensitivity = leave_one_donor_out_moments(fit, [40, 55, 35])

No refitting occurs

This helper removes one fitted donor from each posterior mixture, renormalizes weights, and recomputes moments. It measures sensitivity of the fitted mixture summary. It is not leave-one-out predictive validation and does not include posterior changes that a refit would produce.

Condition contrasts

Conditions fitted independently do not have paired chain/draw labels. Use Cartesian comparisons:

import pandas as pd

from barracuda import condition_contrast_frame, summarize_contrast_draws

all_draws = pd.concat([control_draws, treatment_draws], ignore_index=True)
contrasts = condition_contrast_frame(
    all_draws,
    treatment="Treatment",
    control="Control",
    scale="absolute",
)
contrast_summary = summarize_contrast_draws(contrasts, hdi_prob=0.95)

Exact Cartesian differences are used below the configured pair ceiling; otherwise the function independently samples treatment/control indices with a recorded deterministic seed. Percentage contrasts divide by the fixed control posterior mean, never by individual near-zero particles. When model_key or donor_id columns are present, both contrast construction and summary retain them as groups by default so draws from different fitted models or donors are not silently pooled. Pass an explicit empty group_columns sequence only when pooling is the intended estimand.