Skip to content

quivers.formulas

A brms-style formula frontend over the QVR DSL. It compiles a regression formula to .qvr source and fits the resulting Bayesian model.

formulas

Brms-style formula frontend over the QVR DSL.

A user writes a regression formula (Wilkinson notation, extended with brms-style random-effect groups) and gets a fitted Bayesian model without hand-writing .qvr source. The compiler emits programs that route through the existing QVR DSL surface:

  • fixed-effect linear predictors as morphism composition X >> beta over observed design-matrix morphisms,
  • random-effect groups as plate-gather of per-level latent draws,
  • responses as observe sites with the family-link kernel (Gaussian / Bernoulli / Binomial / Categorical / Poisson / NegativeBinomial / Cumulative / Beta / Gamma / Student-t / ZeroInflatedPoisson / HurdlePoisson / Mixture) registered in quivers.formulas.family.

The implementation reuses formulae for formula parsing (the Bambi team's pure-Python parser; supports brms-style (slope | group) random effects, smooth terms, and custom contrasts) and lifts the resulting DesignMatrices into a typed Formula didactic.api.Model.

The frontend is the formula→QVR direction of a panproto lens; the QVR DSL is the canonical source of truth, and the formula compiler is a structure-preserving translation from the smaller formula language to the QVR DSL.

links: Mapping[str, Link] = {'identity': _IDENTITY, 'logit': _LOGIT, 'log': _LOG, 'softmax': _SOFTMAX, 'inverse': _INVERSE}

The built-in inverse links, keyed by link name.

families module-attribute

families: Mapping[str, Family] = {'gaussian': Family(name='gaussian', location_link=_IDENTITY, observe_family='Normal', extra_observe_args=('sigma',), aux_params=(AuxParam(name='sigma', prior='HalfCauchy(2.0)', link=_LOG),)), 'bernoulli': Family(name='bernoulli', location_link=_LOGIT, observe_family='Bernoulli'), 'binomial': Family(name='binomial', location_link=_LOGIT, observe_family='Binomial'), 'categorical': Family(name='categorical', location_link=_SOFTMAX, observe_family='Categorical'), 'poisson': Family(name='poisson', location_link=_LOG, observe_family='Poisson'), 'negative_binomial': Family(name='negative_binomial', location_link=_LOG, observe_family='NegativeBinomial', extra_observe_args=('disp',), aux_params=(AuxParam(name='disp', prior='Gamma(2.0, 2.0)', link=_LOG),)), 'gamma': Family(name='gamma', location_link=_LOG, observe_family='Gamma', extra_observe_args=('shape',), aux_params=(AuxParam(name='shape', prior='Gamma(2.0, 2.0)', link=_LOG),)), 'beta': Family(name='beta', location_link=_LOGIT, observe_family='Beta', extra_observe_args=('phi',), aux_params=(AuxParam(name='phi', prior='HalfCauchy(2.0)', link=_LOG),)), 'student_t': Family(name='student_t', location_link=_IDENTITY, observe_family='StudentT', extra_observe_args=('nu', 'sigma'), aux_params=(AuxParam(name='nu', prior='Gamma(2.0, 0.1)', link=_LOG), AuxParam(name='sigma', prior='HalfCauchy(2.0)', link=_LOG))), 'cumulative': Family(name='cumulative', location_link=_IDENTITY, observe_family='OrderedLogistic'), 'zero_inflated_poisson': Family(name='zero_inflated_poisson', location_link=_LOG, observe_family='ZeroInflatedPoisson', extra_observe_args=('zi',), aux_params=(AuxParam(name='zi', prior='Beta(2.0, 2.0)', link=_LOGIT),)), 'hurdle_poisson': Family(name='hurdle_poisson', location_link=_LOG, observe_family='HurdlePoisson', extra_observe_args=('zi',), aux_params=(AuxParam(name='zi', prior='Beta(2.0, 2.0)', link=_LOGIT),)), 'zero_one_inflated_beta': Family(name='zero_one_inflated_beta', location_link=_LOGIT, observe_family='ZeroOneInflatedBeta', extra_observe_args=('phi', 'zoi', 'coi'), aux_params=(AuxParam(name='phi', prior='HalfCauchy(2.0)', link=_LOG), AuxParam(name='zoi', prior='Beta(2.0, 2.0)', link=_LOGIT), AuxParam(name='coi', prior='Beta(2.0, 2.0)', link=_LOGIT))), 'mixture': Family(name='mixture', location_link=_IDENTITY, observe_family='MixtureNormal')}

The built-in response families, keyed by brms-style family name.

The compiler dispatches on the family name and slots each family's observe step and link into the emitted .qvr source.

FixedColumn didactic-model

FixedColumn(*, term: str, name: str, qvr_name: str, is_intercept: bool = False, data: ndarray = field(opaque=True))

Bases: Model

One column of the fixed-effects design matrix.

ATTRIBUTE DESCRIPTION
term

Originating term name (e.g. "poly(x, 2)" or "x").

TYPE: str

name

Per-column label, equal to term for single-column terms and f"{term}_{k+1}" (1-indexed, matching R's display) for multi-column terms like poly(x, 2).

TYPE: str

qvr_name

QVR-legal identifier derived from name (alnum / _ only); used as the variable name in the emitted program.

TYPE: str

is_intercept

True for the constant-1 column.

TYPE: bool

RandomTerm didactic-model

RandomTerm(*, slope: str, group: str)

Bases: Model

One random-effect group, e.g. (1 | g) or (x | g).

ATTRIBUTE DESCRIPTION
slope

"Intercept" for (1 | g); otherwise the slope variable name.

TYPE: str

group

Grouping factor name.

TYPE: str

Formula didactic-model

Formula(*, formula: str, response_name: str, fixed_columns: tuple[FixedColumn, ...] = field(default_factory=tuple, opaque=True), random_terms: tuple[RandomTerm, ...] = (), response_values: ndarray = field(opaque=True), group_levels: Mapping[str, tuple[str, ...]] = field(default_factory=dict, opaque=True), group_indices: Mapping[str, tuple[int, ...]] = field(default_factory=dict, opaque=True))

Bases: Model

A parsed regression formula plus the data it was parsed against.

ATTRIBUTE DESCRIPTION
formula

Original formula string.

TYPE: str

response_name

Name of the response column.

TYPE: str

fixed_columns

One entry per design-matrix column (matches R/brms's one-coefficient-per-column convention).

TYPE: tuple[FixedColumn, ...]

random_terms

Random-effect group specifications.

TYPE: tuple[RandomTerm, ...]

response_values

Response column values, shape (N,).

TYPE: ndarray

group_levels

Canonical level ordering per grouping factor, used to derive deterministic plate-index tensors.

TYPE: Mapping[str, tuple[str, ...]]

group_indices

Per-group integer index array, shape (N,).

TYPE: Mapping[str, tuple[int, ...]]

FormulaData didactic-model

FormulaData(*, formula: str = '', response_name: str = '', response_values: ndarray = field(opaque=True), fixed_column_names: Mapping[str, tuple[str, str]] = field(default_factory=dict, opaque=True), fixed_column_data: Mapping[str, ndarray] = field(default_factory=dict, opaque=True), group_original_names: Mapping[str, str] = field(default_factory=dict, opaque=True), group_levels: Mapping[str, tuple[str, ...]] = field(default_factory=dict, opaque=True), group_indices: Mapping[str, tuple[int, ...]] = field(default_factory=dict, opaque=True))

Bases: Model

Store the nonstructural complement of a Formula lens mapping.

The emitted QVR module records formula structure under normalized identifiers. This complement retains the source formula, original names, presentation labels, per-row arrays, group levels, and group indices so backward(module, complement) can reconstruct the original Formula.

ATTRIBUTE DESCRIPTION
formula

Original formula string.

TYPE: str

response_name

Original response column name.

TYPE: str

response_values

Response values with shape (N,).

TYPE: ndarray

fixed_column_names

(term, name) pairs keyed by normalized fixed-column name.

TYPE: Mapping[str, tuple[str, str]]

fixed_column_data

Predictor values keyed by normalized fixed-column name.

TYPE: Mapping[str, ndarray]

group_original_names

Original group names keyed by normalized group name.

TYPE: Mapping[str, str]

group_levels

Canonical level order for each group.

TYPE: Mapping[str, tuple[str, ...]]

group_indices

Per-row integer codes for each group.

TYPE: Mapping[str, tuple[int, ...]]

Link(*, name: str, inverse_expr: str)

Bases: Model

Inverse-link function bridging a linear predictor on :math:\mathbb{R} to the family's natural parameter space.

ATTRIBUTE DESCRIPTION
name

Canonical name (e.g. "identity", "logit", "log", "probit", "cloglog").

TYPE: str

inverse_expr

QVR let-expression template applied to a linear predictor eta to produce the family parameter. Substitution token {eta} is replaced by the linear predictor's variable name. "{eta}" itself is the identity link.

TYPE: str

AuxParam didactic-model

AuxParam(*, name: str, prior: str, link: Link)

Bases: Model

An auxiliary family parameter (scale, dispersion, etc.).

ATTRIBUTE DESCRIPTION
name

Variable name to emit in the .qvr source.

TYPE: str

prior

Prior distribution expression (e.g. "HalfCauchy(2.0)").

TYPE: str

link

Link applied to a per-row linear predictor if the parameter is itself distributionally regressed (y ~ x, sigma ~ x, etc.); otherwise unused.

TYPE: Link

Family didactic-model

Family(*, name: str, location_link: Link, observe_family: str, extra_observe_args: tuple[str, ...] = (), aux_params: tuple[AuxParam, ...] = ())

Bases: Model

Response family: observation kernel plus its link and any auxiliary parameters.

The compiler emits, for each family-keyed regression:

  1. The auxiliary-parameter latent draws (intercept-only by default; one per parameter named in aux_params).
  2. A let eta = <linear predictor> binding the location's linear predictor.
  3. A let mu = <link.inverse_expr> binding the natural parameter.
  4. A single observe y : Resp <- <observe_family>(mu, ...) step parameterised by mu and the auxiliary parameters.

FormulaToQVRModule

FormulaToQVRModule(family: Family, *, fixed_prior: str = 'Normal(0.0, 5.0)', random_scale_prior: str = 'HalfNormal(1.0)', user_priors: Mapping[str, str] | None = None, reparameterize: Literal['centered', 'noncentered'] = 'noncentered', binomial_trials: int | str = 1, thresholds_by: str | None = None, mixture_components: int = 2, predictor_name: str | None = None)

Bases: Lens[Formula, Module, FormulaData]

Translate a Formula to a QVR Module AST.

A typed didactic.api.Lens whose complement is a FormulaData carrier containing the fields of the source Formula that are not recoverable from the emitted Module. The per-row data arrays, the original (pre- _qvr_name) identifiers, the per-column term / name presentation labels, and the original formula string travel in the complement; everything else (which columns there are, the intercept / random-term structure, the family choice) is decoded back out of the Module by _decode_module.

PARAMETER DESCRIPTION
family

Response family from quivers.formulas.families.

TYPE: Family

fixed_prior

Default prior for fixed-effect coefficients, in the surface form "Family(arg, arg, ...)"; numeric args become floats, identifier args stay as variable references in the emitted program.

TYPE: str DEFAULT: 'Normal(0.0, 5.0)'

random_scale_prior

Default prior for random-effect scale parameters.

TYPE: str DEFAULT: 'HalfNormal(1.0)'

user_priors

Per-name prior overrides keyed by the latent's variable name in the emitted module.

TYPE: Mapping[str, str] DEFAULT: None

reparameterize

Random-effect parameterization. Categorical random effects use the non-centered form.

TYPE: ('centered', 'noncentered') DEFAULT: "centered"

binomial_trials

Common trial count or the name of a per-row trial-count column.

TYPE: int or str DEFAULT: 1

thresholds_by

Grouping factor whose levels receive partially pooled ordinal cutpoint spacings.

TYPE: str or None DEFAULT: None

mixture_components

Number of components for family="mixture"; must be at least two.

TYPE: int DEFAULT: 2

predictor_name

Host-data name for an external differentiable predictor.

TYPE: str or None DEFAULT: None

RAISES DESCRIPTION
ValueError

If an option is out of range or does not apply to family: thresholds_by outside the cumulative family, binomial_trials outside the binomial family or below one, mixture_components below two or outside the mixture family, or an unknown reparameterize value.

Notes

GetPut: backward (forward(f)) = f for every Formula f. PutGet holds on pairs (t, c) for which t is in the image of forward and c is the corresponding FormulaData.

Source code in src/quivers/formulas/compile.py
def __init__(
    self,
    family: Family,
    *,
    fixed_prior: str = "Normal(0.0, 5.0)",
    random_scale_prior: str = "HalfNormal(1.0)",
    user_priors: Mapping[str, str] | None = None,
    reparameterize: Literal["centered", "noncentered"] = "noncentered",
    binomial_trials: int | str = 1,
    thresholds_by: str | None = None,
    mixture_components: int = 2,
    predictor_name: str | None = None,
) -> None:
    self._family = family
    self._fixed_prior = fixed_prior
    self._random_scale_prior = random_scale_prior
    self._user_priors: Mapping[str, str] = dict(user_priors or {})
    if thresholds_by is not None and family.name != "cumulative":
        raise ValueError(
            "FormulaToQVRModule: thresholds_by is only valid for the "
            "cumulative family"
        )
    if family.name != "binomial" and binomial_trials != 1:
        raise ValueError(
            "FormulaToQVRModule: binomial_trials is only valid for the "
            "binomial family"
        )
    if not isinstance(binomial_trials, (int, str)) or isinstance(
        binomial_trials, bool
    ):
        raise ValueError(
            "FormulaToQVRModule: binomial_trials must be a positive integer "
            "or a data-column name"
        )
    if isinstance(binomial_trials, int) and binomial_trials < 1:
        raise ValueError("FormulaToQVRModule: binomial_trials must be at least one")
    if (
        not isinstance(mixture_components, int)
        or isinstance(mixture_components, bool)
        or mixture_components < 2
    ):
        raise ValueError(
            "FormulaToQVRModule: mixture_components must be an integer "
            "of at least two"
        )
    if family.name != "mixture" and mixture_components != 2:
        raise ValueError(
            "FormulaToQVRModule: mixture_components is only valid for the "
            "mixture family"
        )
    self._binomial_trials = binomial_trials
    self._thresholds_by = thresholds_by
    self._mixture_components = mixture_components
    self._predictor_name = (
        _qvr_name(predictor_name) if predictor_name is not None else None
    )
    if reparameterize not in ("centered", "noncentered"):
        raise ValueError(
            f"FormulaToQVRModule: reparameterize must be 'centered' "
            f"or 'noncentered', got {reparameterize!r}"
        )
    self._reparameterize = reparameterize

forward

forward(formula: Formula) -> tuple[Module, FormulaData]

Compile a formula to a QVR module and its complement.

PARAMETER DESCRIPTION
formula

The parsed formula with its design data.

TYPE: Formula

RETURNS DESCRIPTION
tuple[Module, FormulaData]

The emitted QVR module AST, and the fields of formula the module does not encode.

RAISES DESCRIPTION
ValueError

If the response is incompatible with the family (for instance non-integer or non-contiguous category labels for a categorical or cumulative family), or a prior does not parse as "Family(arg, ...)".

Source code in src/quivers/formulas/compile.py
def forward(self, formula: Formula, /) -> tuple[Module, FormulaData]:
    """Compile a formula to a QVR module and its complement.

    Parameters
    ----------
    formula : Formula
        The parsed formula with its design data.

    Returns
    -------
    tuple[Module, FormulaData]
        The emitted QVR module AST, and the fields of ``formula``
        the module does not encode.

    Raises
    ------
    ValueError
        If the response is incompatible with the family (for
        instance non-integer or non-contiguous category labels
        for a categorical or cumulative family), or a prior does
        not parse as ``"Family(arg, ...)"``.
    """
    module = self._build_module(formula)
    complement = FormulaData(
        formula=formula.formula,
        response_name=formula.response_name,
        response_values=formula.response_values,
        fixed_column_names={
            col.qvr_name: (col.term, col.name) for col in formula.fixed_columns
        },
        fixed_column_data={
            col.qvr_name: col.data
            for col in formula.fixed_columns
            if not col.is_intercept
        },
        group_original_names={_qvr_name(g): g for g in formula.group_levels.keys()},
        group_levels=dict(formula.group_levels),
        group_indices=dict(formula.group_indices),
    )
    return module, complement

backward

backward(target: Module, complement: FormulaData) -> Formula

Recover a formula from a QVR module and its complement.

The column, intercept, and random-term structure is decoded from target; the data arrays and presentation labels come from complement.

PARAMETER DESCRIPTION
target

A QVR module in the image of forward.

TYPE: Module

complement

The complement forward returned with that module.

TYPE: FormulaData

RETURNS DESCRIPTION
Formula

The reconstructed formula.

Source code in src/quivers/formulas/compile.py
def backward(self, target: Module, complement: FormulaData, /) -> Formula:
    """Recover a formula from a QVR module and its complement.

    The column, intercept, and random-term structure is decoded
    from ``target``; the data arrays and presentation labels come
    from ``complement``.

    Parameters
    ----------
    target : Module
        A QVR module in the image of `forward`.
    complement : FormulaData
        The complement `forward` returned with that module.

    Returns
    -------
    Formula
        The reconstructed formula.
    """
    decoded = _decode_module(target)
    fixed_column_names = complement.fixed_column_names
    fixed_column_data = complement.fixed_column_data

    fixed_columns: list[FixedColumn] = []
    n_obs = decoded["n_obs"]
    ones = np.ones(n_obs, dtype=float)
    for qvr_name, is_intercept in decoded["fixed_qvr_names"]:
        if is_intercept:
            term, name = fixed_column_names.get(
                "intercept", ("Intercept", "Intercept")
            )
            fixed_columns.append(
                FixedColumn(
                    term=term,
                    name=name,
                    qvr_name="intercept",
                    is_intercept=True,
                    data=ones,
                )
            )
        else:
            term, name = fixed_column_names.get(qvr_name, (qvr_name, qvr_name))
            fixed_columns.append(
                FixedColumn(
                    term=term,
                    name=name,
                    qvr_name=qvr_name,
                    is_intercept=False,
                    data=fixed_column_data[qvr_name],
                )
            )

    random_terms: list[RandomTerm] = []
    for qgroup, slope_qvr in decoded["random_terms_qvr"]:
        group = complement.group_original_names.get(qgroup, qgroup)
        if slope_qvr == "Intercept":
            random_terms.append(RandomTerm(slope="Intercept", group=group))
        else:
            # Recover the original slope name by inverting via the
            # presentation map when available; otherwise pass the
            # qvr-name through.
            term_name_pairs = {
                qvr: (term, name)
                for qvr, (term, name) in fixed_column_names.items()
            }
            _, original_slope = term_name_pairs.get(
                slope_qvr, (slope_qvr, slope_qvr)
            )
            random_terms.append(RandomTerm(slope=original_slope, group=group))

    return Formula(
        formula=complement.formula,
        response_name=complement.response_name,
        fixed_columns=tuple(fixed_columns),
        random_terms=tuple(random_terms),
        response_values=complement.response_values,
        group_levels=complement.group_levels,
        group_indices=complement.group_indices,
    )

fixed_column_observations

fixed_column_observations(formula: Formula) -> dict[str, Tensor]

Per-column free-variable bindings for the host-data channel. One entry per non-intercept fixed column, shape (N,).

PARAMETER DESCRIPTION
formula

The formula whose fixed-effect columns are bound.

TYPE: Formula

RETURNS DESCRIPTION
dict[str, Tensor]

Each non-intercept column's float32 data, keyed by its QVR name.

Source code in src/quivers/formulas/compile.py
def fixed_column_observations(self, formula: Formula) -> dict[str, torch.Tensor]:
    """Per-column free-variable bindings for the host-data
    channel.  One entry per non-intercept fixed column, shape
    ``(N,)``.

    Parameters
    ----------
    formula : Formula
        The formula whose fixed-effect columns are bound.

    Returns
    -------
    dict[str, torch.Tensor]
        Each non-intercept column's ``float32`` data, keyed by its
        QVR name.
    """
    obs: dict[str, torch.Tensor] = {}
    for col in formula.fixed_columns:
        if col.is_intercept:
            continue
        obs[col.qvr_name] = torch.as_tensor(col.data.copy(), dtype=torch.float32)
    return obs

BayesianFit didactic-model

BayesianFit(*, formula: Formula = field(opaque=True), family: Family, program: MonadicProgram = field(opaque=True), posterior: MCMCResult | Guide = field(opaque=True), observations: Mapping[str, Tensor] = field(default_factory=dict, opaque=True), reparameterize: Literal['centered', 'noncentered'] = 'noncentered', binomial_trials: int | str = 1, thresholds_by: str | None = None, mixture_components: int = 2, fixed_prior: str = 'Normal(0.0, 5.0)', random_scale_prior: str = 'HalfNormal(1.0)', priors: Mapping[str, str] = field(default_factory=dict, opaque=True), predictor: Module | None = field(default=None, opaque=True), predictor_data: Tensor | None = field(default=None, opaque=True), predictor_name: str | None = None)

Bases: Model

A fitted Bayesian regression: the compiled program, the parsed formula, the family, the user-supplied data, and the posterior samples.

ATTRIBUTE DESCRIPTION
formula

Parsed formula IR.

TYPE: Formula

family

Response family used at compile time.

TYPE: Family

program

The compiled program.

TYPE: MonadicProgram

posterior

TYPE: MCMCResult or Guide

observations

Inference-time observations dict (response + per-column covariates + per-group plate indices).

TYPE: Mapping[str, Tensor]

qvr_source property

qvr_source: str

Lazily emit the AST-equivalent .qvr source for display.

dump_qvr

dump_qvr(path: str | Path) -> Path

Write the AST-equivalent .qvr source to path and return the resulting Path.

Source code in src/quivers/formulas/_fit.py
def dump_qvr(self, path: str | Path) -> Path:
    """Write the AST-equivalent ``.qvr`` source to ``path`` and
    return the resulting `Path`.
    """
    out = Path(path)
    out.write_text(self.qvr_source)
    return out

formula_from_data

formula_from_data(formula: str, data: IntoDataFrame, *, extra_namespace: Mapping[str, object] | None = None) -> Formula

Build a typed Formula IR by lifting formulae.design_matrices over a dataframe.

This is an adapter, not a parser: the brms-style formula syntax is parsed by the formulae library; we lift its formulae.matrices.DesignMatrices result into a typed didactic record, augmented with deterministic per-group level orderings and integer-code arrays derived from the dataframe.

The R-style numeric transforms (log, exp, sqrt, abs, sin, cos, tan, log10, log2, log1p, expm1, asin, acos, atan, sinh, cosh, tanh) are pre-loaded into the formulae evaluation namespace so users coming from R / brms get the expected base R behaviour without explicit registration. Polynomial terms via poly(x, k) are orthogonal by default, matching R's stats::poly.

PARAMETER DESCRIPTION
formula

Formula string in brms / lme4 syntax.

TYPE: str

data

Pandas, polars, or any other Narwhals-compatible dataframe.

TYPE: IntoDataFrame

extra_namespace

Additional names visible inside the formula's expression evaluation, merged on top of the R-style transforms.

TYPE: Mapping[str, object] DEFAULT: None

RETURNS DESCRIPTION
Formula

The formula IR: response values, one fixed column per design column, the random terms, and each grouping factor's levels and per-row level indices.

RAISES DESCRIPTION
ValueError

If the formula has no response on the left of ~, or a random term is not of the form (slope | group).

Source code in src/quivers/formulas/formula.py
def formula_from_data(
    formula: str,
    data: IntoDataFrame,
    *,
    extra_namespace: Mapping[str, object] | None = None,
) -> Formula:
    """Build a typed `Formula` IR by lifting
    `formulae.design_matrices` over a dataframe.

    This is an adapter, not a parser: the brms-style formula syntax
    is parsed by the [`formulae`](https://bambinos.github.io/formulae/)
    library; we lift its `formulae.matrices.DesignMatrices`
    result into a typed didactic record, augmented with deterministic
    per-group level orderings and integer-code arrays derived from
    the dataframe.

    The R-style numeric transforms (``log``, ``exp``, ``sqrt``,
    ``abs``, ``sin``, ``cos``, ``tan``, ``log10``, ``log2``,
    ``log1p``, ``expm1``, ``asin``, ``acos``, ``atan``, ``sinh``,
    ``cosh``, ``tanh``) are pre-loaded into the formulae evaluation
    namespace so users coming from R / brms get the expected base
    R behaviour without explicit registration.  Polynomial terms via
    ``poly(x, k)`` are orthogonal by default, matching R's
    ``stats::poly``.

    Parameters
    ----------
    formula : str
        Formula string in brms / lme4 syntax.
    data : IntoDataFrame
        Pandas, polars, or any other Narwhals-compatible dataframe.
    extra_namespace : Mapping[str, object], optional
        Additional names visible inside the formula's expression
        evaluation, merged on top of the R-style transforms.

    Returns
    -------
    Formula
        The formula IR: response values, one fixed column per design
        column, the random terms, and each grouping factor's levels
        and per-row level indices.

    Raises
    ------
    ValueError
        If the formula has no response on the left of ``~``, or a
        random term is not of the form ``(slope | group)``.
    """
    nw_df = nw.from_native(data, eager_only=True)
    pandas_df = nw_df.to_pandas()
    namespace: dict[str, object] = dict(_R_TRANSFORMS)
    if extra_namespace:
        namespace.update(extra_namespace)
    dm = fo.design_matrices(formula, data=pandas_df, extra_namespace=namespace)
    if dm.response is None:
        raise ValueError(
            f"formula_from_data: formula {formula!r} has no response "
            f"variable on the left of `~`"
        )
    response_name = dm.response.name
    n_obs = int(pandas_df.shape[0])

    fixed_columns: list[FixedColumn] = []
    if dm.common is not None:
        for term_name, term in dm.common.terms.items():
            fixed_columns.extend(_explode_term(term_name, term, n_obs))

    random_terms: list[RandomTerm] = []
    group_levels: dict[str, tuple[str, ...]] = {}
    group_indices: dict[str, tuple[int, ...]] = {}
    if dm.group is not None:
        for term_name in dm.group.terms.keys():
            if "|" not in term_name:
                raise ValueError(
                    f"formula_from_data: unexpected random term name "
                    f"{term_name!r}; expected `(slope | group)` syntax"
                )
            slope, group = term_name.split("|", 1)
            slope = slope.strip()
            group = group.strip()
            if slope == "1":
                slope = "Intercept"
            random_terms.append(RandomTerm(slope=slope, group=group))
            if group not in group_levels:
                levels = tuple(
                    str(v) for v in nw_df[group].drop_nulls().unique().sort().to_list()
                )
                group_levels[group] = levels
                level_index = {v: i for i, v in enumerate(levels)}
                codes = tuple(level_index[str(v)] for v in nw_df[group].to_list())
                group_indices[group] = codes

    response_values = (
        np.asarray(dm.response.design_matrix).reshape(-1).astype(np.float64)
    )

    return Formula(
        formula=formula,
        response_name=response_name,
        fixed_columns=tuple(fixed_columns),
        random_terms=tuple(random_terms),
        response_values=response_values,
        group_levels=group_levels,
        group_indices=group_indices,
    )

fit

fit(formula: str, *, data: IntoDataFrame, family: str | Family = 'gaussian', method: Literal['nuts', 'hmc', 'svi'] = 'nuts', num_warmup: int = 500, num_samples: int = 1000, num_chains: int = 4, fixed_prior: str = 'Normal(0.0, 5.0)', random_scale_prior: str = 'HalfNormal(1.0)', priors: Mapping[str, str] | None = None, guide: type | None = None, reparameterize: Literal['centered', 'noncentered'] = 'noncentered', binomial_trials: int | str = 1, thresholds_by: str | None = None, mixture_components: int = 2, predictor: Module | None = None, predictor_data: Tensor | None = None, seed: int = 0) -> BayesianFit

Compile a brms-style formula, fit it, and return the result.

See quivers.formulas for surface details. This entry point composes formula_from_data, FormulaToQVRModule, Compiler, and the inference layer in one call.

family="categorical" infers contiguous labels 0..K-1 and uses label zero as the reference. An attached categorical predictor may return (N, K-1) reference logits or (N, K) full logits. For family="mixture", mixture_components selects any finite component count of at least two; the likelihood integrates assignments exactly.

PARAMETER DESCRIPTION
formula

brms/lme4-style response formula.

TYPE: str

data

Pandas, Polars, or another Narwhals-compatible dataframe.

TYPE: IntoDataFrame

family

Registered response family or a family value.

TYPE: str or Family DEFAULT: 'gaussian'

method

Inference algorithm.

TYPE: ('nuts', 'hmc', 'svi') DEFAULT: "nuts"

mixture_components

Number of Gaussian components for the mixture family.

TYPE: int DEFAULT: 2

predictor

Optional differentiable contribution on the linear-predictor scale.

TYPE: Module or None DEFAULT: None

predictor_data

Input passed to predictor on each SVI step.

TYPE: Tensor or None DEFAULT: None

RETURNS DESCRIPTION
BayesianFit

Parsed formula, compiled program, fitted posterior, and observations.

Source code in src/quivers/formulas/_fit.py
def fit(
    formula: str,
    *,
    data: IntoDataFrame,
    family: str | Family = "gaussian",
    method: Literal["nuts", "hmc", "svi"] = "nuts",
    num_warmup: int = 500,
    num_samples: int = 1000,
    num_chains: int = 4,
    fixed_prior: str = "Normal(0.0, 5.0)",
    random_scale_prior: str = "HalfNormal(1.0)",
    priors: Mapping[str, str] | None = None,
    guide: type | None = None,
    reparameterize: Literal["centered", "noncentered"] = "noncentered",
    binomial_trials: int | str = 1,
    thresholds_by: str | None = None,
    mixture_components: int = 2,
    predictor: nn.Module | None = None,
    predictor_data: torch.Tensor | None = None,
    seed: int = 0,
) -> BayesianFit:
    """Compile a brms-style formula, fit it, and return the result.

    See [`quivers.formulas`][quivers.formulas] for surface details.  This entry
    point composes `formula_from_data`, `FormulaToQVRModule`,
    `Compiler`, and the inference layer in one call.

    ``family="categorical"`` infers contiguous labels ``0..K-1`` and uses
    label zero as the reference. An attached categorical predictor may return
    ``(N, K-1)`` reference logits or ``(N, K)`` full logits. For
    ``family="mixture"``, ``mixture_components`` selects any finite component
    count of at least two; the likelihood integrates assignments exactly.

    Parameters
    ----------
    formula : str
        brms/lme4-style response formula.
    data : IntoDataFrame
        Pandas, Polars, or another Narwhals-compatible dataframe.
    family : str or Family
        Registered response family or a family value.
    method : {"nuts", "hmc", "svi"}
        Inference algorithm.
    mixture_components : int
        Number of Gaussian components for the mixture family.
    predictor : nn.Module or None
        Optional differentiable contribution on the linear-predictor scale.
    predictor_data : torch.Tensor or None
        Input passed to ``predictor`` on each SVI step.

    Returns
    -------
    BayesianFit
        Parsed formula, compiled program, fitted posterior, and observations.
    """
    if isinstance(family, str):
        if family not in families:
            raise ValueError(
                f"fit: unknown family {family!r}; choices are {sorted(families)}"
            )
        family_obj = families[family]
    else:
        family_obj = family

    parsed = formula_from_data(formula, data)
    predictor_name = "neural_eta" if predictor is not None else None
    if predictor is not None and predictor_data is None:
        raise ValueError("fit: predictor_data is required when predictor is supplied")
    if predictor is None and predictor_data is not None:
        raise ValueError("fit: predictor_data requires predictor")
    if (
        predictor is not None
        and method != "svi"
        and any(parameter.requires_grad for parameter in predictor.parameters())
    ):
        raise ValueError(
            "fit: a trainable external predictor requires method='svi'; "
            "freeze its parameters before using NUTS or HMC"
        )
    lens = FormulaToQVRModule(
        family_obj,
        fixed_prior=fixed_prior,
        random_scale_prior=random_scale_prior,
        user_priors=priors,
        reparameterize=reparameterize,
        binomial_trials=binomial_trials,
        thresholds_by=thresholds_by,
        mixture_components=mixture_components,
        predictor_name=predictor_name,
    )
    module, _ = lens.forward(parsed)
    compiler = Compiler(module)
    program_runtime = compiler.compile()
    morphism = program_runtime.morphism
    if not isinstance(morphism, MonadicProgram):
        raise TypeError(
            f"fit: compiled morphism has type "
            f"{type(morphism).__name__}, expected MonadicProgram"
        )
    program = morphism

    observations: dict[str, torch.Tensor] = {}
    observations.update(lens.fixed_column_observations(parsed))
    response_name = parsed.response_name
    observations[response_name] = torch.as_tensor(
        parsed.response_values.copy(), dtype=torch.float32
    ).reshape(-1)
    for group, codes in parsed.group_indices.items():
        observations[f"{_qvr_name(group)}_idx"] = torch.as_tensor(
            list(codes), dtype=torch.long
        )
    if family_obj.name == "binomial":
        trials = _binomial_trial_tensor(
            data, binomial_trials, n_obs=parsed.response_values.shape[0]
        )
        response = observations[response_name]
        _validate_binomial_response(response, trials, caller="fit")
        if isinstance(binomial_trials, str):
            observations[_qvr_name(binomial_trials)] = trials
    if predictor is not None:
        assert predictor_data is not None
        assert predictor_name is not None
        predictor_categories = (
            FormulaToQVRModule._category_count(parsed, family="categorical")
            if family_obj.name == "categorical"
            else None
        )
        observations[predictor_name] = _predictor_output(
            predictor,
            predictor_data,
            n_obs=parsed.response_values.shape[0],
            n_categories=predictor_categories,
        ).detach()
    else:
        predictor_categories = None

    torch.manual_seed(seed)
    if method == "svi":
        posterior = _fit_svi(
            program,
            observations,
            num_samples,
            guide_cls=guide,
            predictor=predictor,
            predictor_data=predictor_data,
            predictor_name=predictor_name,
            predictor_categories=predictor_categories,
        )
    else:
        posterior = _fit_mcmc(
            program,
            observations,
            sampler=method,
            num_warmup=num_warmup,
            num_samples=num_samples,
            num_chains=num_chains,
        )

    return BayesianFit(
        formula=parsed,
        family=family_obj,
        program=program,
        posterior=posterior,
        observations=observations,
        reparameterize=reparameterize,
        binomial_trials=binomial_trials,
        thresholds_by=thresholds_by,
        mixture_components=mixture_components,
        fixed_prior=fixed_prior,
        random_scale_prior=random_scale_prior,
        priors=dict(priors or {}),
        predictor=predictor,
        predictor_data=predictor_data,
        predictor_name=predictor_name,
    )

formula_to_qvr

formula_to_qvr(formula: str, *, data: IntoDataFrame, family: str | Family = 'gaussian', fixed_prior: str = 'Normal(0.0, 5.0)', random_scale_prior: str = 'HalfNormal(1.0)', priors: Mapping[str, str] | None = None, reparameterize: Literal['centered', 'noncentered'] = 'noncentered', binomial_trials: int | str = 1, thresholds_by: str | None = None, mixture_components: int = 2, predictor_name: str | None = None, path: str | Path | None = None) -> str

Emit .qvr source for a brms-style formula without fitting.

Builds the formula AST → QVR Module via FormulaToQVRModule, then serialises the module via quivers.dsl.emit.module_to_source. Optionally writes the result to path. mixture_components selects any integer component count of at least two for family="mixture".

Source code in src/quivers/formulas/_fit.py
def formula_to_qvr(
    formula: str,
    *,
    data: IntoDataFrame,
    family: str | Family = "gaussian",
    fixed_prior: str = "Normal(0.0, 5.0)",
    random_scale_prior: str = "HalfNormal(1.0)",
    priors: Mapping[str, str] | None = None,
    reparameterize: Literal["centered", "noncentered"] = "noncentered",
    binomial_trials: int | str = 1,
    thresholds_by: str | None = None,
    mixture_components: int = 2,
    predictor_name: str | None = None,
    path: str | Path | None = None,
) -> str:
    """Emit ``.qvr`` source for a brms-style formula without fitting.

    Builds the formula AST → QVR Module via `FormulaToQVRModule`,
    then serialises the module via [`quivers.dsl.emit.module_to_source`][quivers.dsl.emit.module_to_source].
    Optionally writes the result to ``path``. ``mixture_components`` selects
    any integer component count of at least two for ``family="mixture"``.
    """
    if isinstance(family, str):
        if family not in families:
            raise ValueError(
                f"formula_to_qvr: unknown family {family!r}; choices are "
                f"{sorted(families)}"
            )
        family_obj = families[family]
    else:
        family_obj = family
    parsed = formula_from_data(formula, data)
    if family_obj.name == "binomial":
        trials = _binomial_trial_tensor(
            data, binomial_trials, n_obs=parsed.response_values.shape[0]
        )
        response = torch.as_tensor(parsed.response_values.copy())
        _validate_binomial_response(response, trials, caller="formula_to_qvr")
    lens = FormulaToQVRModule(
        family_obj,
        fixed_prior=fixed_prior,
        random_scale_prior=random_scale_prior,
        user_priors=priors,
        reparameterize=reparameterize,
        binomial_trials=binomial_trials,
        thresholds_by=thresholds_by,
        mixture_components=mixture_components,
        predictor_name=predictor_name,
    )
    module, _ = lens.forward(parsed)
    source = module_to_source(module)
    if path is not None:
        Path(path).write_text(source)
    return source