ergodicDocs
Products: switch from ergodic to another product
Get started

ReferenceStructural causal models

Structural causal models

The SCM pillar: a Markovian Bayesian structural causal model, PyMC under the hood. Import from ergodic.scm.

A SCM is a MixedGraph plus a fitted mechanism per node. Every node is a structural equation over an explicit exogenous-noise variable, so the model answers the three rungs of the causal ladder: observational sampling, pm.do interventions, and the explicit-noise twin-network counterfactual (abduct the unit's noise on the factual graph, predict on an action branch that reuses it).

The honesty thesis is built in. Before answering an effect, the SCM asks the identification layer. An identified effect returns a posterior SCMResult; a non-identified one returns a Sensitivity (the b(c) curve over the assumed confounding), never a naked point, so a caller cannot accidentally read a confident number off an effect the data does not determine.

PyMC is an optional dependency. Importing this package never imports pymc; the import pymc lives inside the methods that fit or query, so the bare install keeps working (the PyMCLearner precedent). Install the extra with pip install 'ergodic[scm]'.

Mechanisms are auto-assigned by the column schema (continuous to a Bayesian-linear Gaussian-additive, binary and categorical to a Gumbel-max), and overridable.

Also exported from ergodic: SCM, BinaryGumbelMax, CategoricalGumbelMax, GaussianAdditive, SCMResult, Sensitivity.

SCM#

python
class SCM(
    graph: MixedGraph,
    data: Dataset,
    *,
    mechanisms: Mapping[str, Any] | None = None,
    draws: int = 1000,
    tune: int = 1000,
    chains: int = 2,
    cores: int = 1,
    random_state: int | None = None,
    district_size_cap: int = 8,
) -> None

A Bayesian structural causal model over a DAG or a latent-confounded ADMG.

Build with SCM(graph, data) (mechanisms auto-assigned by schema) or pass mechanisms={node: spec} to override. spec is a built-in name ("linear", "categorical", "binary") or a Mechanism instance. Call fit(), then query.

The graph may be a Markovian DAG, or an ADMG (<-> edges) / a DAG with latent= confounders. A bidirected edge is realized as one explicit per-unit latent common cause: the confounded district fits jointly (the latent absorbs the confounding) so an effect identified despite the confounding (front-door, the general criterion with the latent modeled) computes correctly. A genuinely non-identified effect still returns a Sensitivity through ate(), never a naked point -- identification, not the fit, decides. Selection bias stays out of scope (identification raises on selection marks). A CPDAG, MAG, PAG, or mixed graph is refused: pick one world first.

draws, tune, and chains set every NUTS run. cores caps the worker processes PyMC may use for the chains (default 1, so chains run one after another and a fit never fans out over the machine); raise it to run chains in parallel. The draws do not depend on it.

SCM.fitted#

python
property SCM.fitted: bool

Whether every node's mechanism has been fitted.

SCM.fit#

python
def SCM.fit(*, parallel: bool = False) -> SCM

Fit every node's mechanism, per node for singletons, jointly per district.

A Markovian singleton district fits per node (cheap and exact); a confounded district (nodes sharing a <->) does not factorize -- the latent couples it -- so it fits as one joint NUTS block with the explicit latents, the gauge, a non-centered parameterization, and a high target_accept. The composed posterior merges the two the same way it merges per-node fits.

Sequential by default (robust, CI-safe). parallel=True spreads the singleton nodes over a process pool with the BLAS/OMP threads pinned in the workers; a confounded district's joint fit stays in the main process (its chains already parallelize and its model is one block). Returns self.

SCM.sample#

python
def SCM.sample(n: int = 1000) -> SCMResult | CategoricalResult

Observational draws of every node (read the target with marginal()).

Returns the posterior predictive of the generative model over n units; the result carries the draws of the last node in topological order (a sink) as a summary. A categorical sink returns a CategoricalResult (the per-category marginal). Use marginal() to read a specific node, or interventional() / counterfactual() for a query on one.

SCM.marginal#

python
def SCM.marginal(
    target: str,
    n: int = 1000,
) -> SCMResult | CategoricalResult

Observational posterior-predictive draws of one node (kind="predictive").

A continuous node returns an SCMResult; a categorical/binary node a CategoricalResult (the per-category marginal), exactly as counterfactual() does for a categorical target.

SCM.do#

python
def SCM.do(intervention: Mapping[str, DoValue]) -> InterventionView

A lightweight intervened view of the SCM for chaining.

scm.do({"X": 1.0}).interventional("Y") reads the post-intervention distribution. The view shares the fit; it does not refit. A continuous do-target takes a float; a categorical/binary do-target takes its integer category code.

SCM.interventional#

python
def SCM.interventional(
    target: str,
    do: Mapping[str, DoValue] | None = None,
    *,
    n: int = 2000,
) -> SCMResult | CategoricalResult

The distribution of target under do, through pm.do.

Returns posterior-predictive draws of the outcome for n units under the intervention: each draw is one unit's Y (parameter uncertainty plus the mechanism's noise). So value (the mean) estimates E[Y | do(X = x)], while std_error and interval() describe the spread of Y itself, not the uncertainty in that mean; the result's kind is "predictive" and its repr says "predictive interval". For an effect with a credible interval on the expectation, use ate().

do maps each intervened node to its value (broadcast to a length-n vector internally, the verified shape gotcha; an integer code for a categorical/binary do-target, a float for a continuous one). target must not be an intervened node. A categorical target returns a CategoricalResult (the post-intervention per-category marginal); a continuous target an SCMResult.

SCM.counterfactual#

python
def SCM.counterfactual(
    units: Dataset | Mapping[str, Any],
    do: Mapping[str, float],
    target: str,
    *,
    use_draws: bool = True,
) -> SCMResult | CategoricalResult

The twin-network counterfactual for the given units under do.

Abduct each unit's structural noise on the factual graph, then predict target on the action branch reusing it. units is a dataset (or a mapping of node name to a column of factual values) holding every node's factual value per unit. For invertible mechanisms the counterfactual is exact (use_draws=False collapses to the deterministic point; the default carries parameter uncertainty). A categorical target returns a CategoricalResult (the answer is a distribution).

SCM.ite#

python
def SCM.ite(
    units: Dataset | Mapping[str, Any],
    treatment: str,
    outcome: str,
    *,
    levels: tuple[float, float] | None = None,
    shift: float | None = None,
) -> ITEResult

Per-unit individual treatment effects, through the twin network.

For each unit, the exact counterfactual contrast of outcome under two treatment worlds that share that unit's abducted noise (the same-person, two-worlds twin network). Returns one effect per unit, ordered as units -- the structural-model sibling of a CATE estimator's cate(), which returns a conditional average over the noise rather than the individual's own counterfactual.

Two framings of the contrast, and you pick one:

The two coincide for a linear-additive mechanism: the shift contrast is the constant coef * delta for every unit, equal to the matching level step. They part only once a mechanism is nonlinear, where the shift is the unit's local response at its own baseline. So with the built-in linear-Gaussian mechanisms the continuous ITE is constant across units (the coefficient), and genuine per-unit spread comes from a binary or categorical outcome, whose logistic response varies with each unit's baseline.

Each effect is the exact point (invertible mechanisms). For an individual's posterior interval, call counterfactual() on that one unit per arm with use_draws=True and difference the draws.

SCM.ate#

python
def SCM.ate(
    treatment: str,
    outcome: str,
    *,
    levels: tuple[float, float] | None = None,
    engine: str = 'posterior',
    n: int = 2000,
) -> SCMResult | Sensitivity

The average treatment effect, routed through identification first.

identify_effect decides the return shape. Identified by back-door adjustment (or the no-path "null" case): the SCM posterior plug-in contrast E[Y | do(X = high)] - E[Y | do(X = low)] (a posterior SCMResult). Not identified: a Sensitivity (the b(c) curve over the assumed confounding), so a naked point is impossible.

The treatment may be continuous, binary, or categorical. levels is the (low, high) contrast; it defaults to (0.0, 1.0) (the two codes of a binary treatment, or a unit step for a continuous one). A multi-category treatment has no canonical pair, so it requires explicit levels (the two category codes to contrast), mirroring the inference pillar's treated= pattern. The outcome must be numeric (a categorical outcome has no scalar contrast and raises); confounders and mediators may be any type -- a categorical one is sampled in the generative model, so the back-door g-formula marginalizes over it correctly.

Identified by back-door adjustment, the no-path "null" case, or front-door computes the latent-aware do-contrast: a confounded district fits its explicit latent, so the structural mechanism on the causal path fits clean and do severs the treatment from the district latent, leaving the identified value (the front-door mediated effect, the C1 world's 1.2). The instrument strategy still raises with a pointer to estimate_effect(): an instrument identifies the effect only under parametric assumptions (a linear, homogeneous effect), not nonparametrically, so the SCM's do-contrast, which claims no such assumption, does not report it. The latent-district fit can land near the IV value, but its posterior then leans on the priors along the confounding ridge. The general ID functional likewise raises (its latent-fit do-contrast was not verified against estimate_effect to tolerance). engine="dml" is reserved for a cross-fit reuse path that is not available; it raises with a pointer.

BinaryGumbelMax#

python
class BinaryGumbelMax(
    name: str,
    parents: tuple[str, ...],
    parent_cards: tuple[int | None, ...] = (),
    n_categories: int = 2,
    coef_prior_sd: float = 3.0,
    latents: tuple[str, ...] = (),
    gauged_latents: tuple[str, ...] = (),
    loading_prior_sd: float = 2.0,
) -> None

A binary node as a two-category Gumbel-max (the logistic mechanism).

Specializes CategoricalGumbelMax to two categories; the marginal is the Bernoulli logistic, and the Gumbel-max counterfactual is the binary common-random-numbers flip.

Field Type Default
name str required
parents tuple[str, ...] required
parent_cards tuple[int | None, ...] ()
n_categories int 2
coef_prior_sd float 3.0
latents tuple[str, ...] ()
gauged_latents tuple[str, ...] ()
loading_prior_sd float 2.0

BinaryGumbelMax.capability#

python
property BinaryGumbelMax.capability: Capability

Always INVERTIBLE: the Gumbel noise can be abducted.

BinaryGumbelMax.weight_name#

python
property BinaryGumbelMax.weight_name: str

The posterior parameter name of the per-class weight matrix, {name}__logit_w.

BinaryGumbelMax.bias_name#

python
property BinaryGumbelMax.bias_name: str

The posterior parameter name of the per-class bias, {name}__logit_b.

BinaryGumbelMax.loading_name#

python
def BinaryGumbelMax.loading_name(latent: str) -> str

The per-class logit loading parameter for this node on latent L.

BinaryGumbelMax.build#

python
def BinaryGumbelMax.build(x: Array, y_codes: IntArray, n_cat: int) -> Any

The per-node PyMC model: y ~ Categorical(logits = bias + x_encoded . weight).

Parameter Description
x The raw (n, p) parent matrix (codes for discrete parents), one-hot encoded here.
y_codes The observed category codes.
n_cat The number of categories.

Returns: A pymc.Model.

BinaryGumbelMax.generative_rv#

python
def BinaryGumbelMax.generative_rv(nodes: Mapping[str, Any], n: int) -> Any

Add this node's categorical RV to the active generative model.

logits = bias + encode(PA) . weight (same per-class linear form and same one-hot parent encoding the fit uses), then pm.Categorical samples the node forward (the Gumbel-max marginal). The RV names match the merged posterior, so sample_posterior_predictive reads the fitted logits. The node's resolved class count must be passed in (it lives on the fit, not the spec). An incident latent L adds a per-class logit term L * loading_L (loading_L shape (K,)), so the confounding shifts the relative class logits, and pm.do severs the node from L.

BinaryGumbelMax.fit#

python
def BinaryGumbelMax.fit(
    data: Dataset,
    *,
    draws: int,
    tune: int,
    chains: int,
    seed: int,
) -> FitResult

Fit this node alone by NUTS and return its parameter posterior.

The class count is n_categories when set, else read from the column's categories. A node in a confounded district is fitted jointly by fit(), not here.

Parameter Description
data The dataset holding the node and its parents.
draws Posterior draws per chain.
tune Tuning steps per chain.
chains Number of chains.
seed The sampler seed.

Returns: A FitResult with the bias and (with parents) weight draws, their posterior means, and the resolved class count.

BinaryGumbelMax.logits#

python
def BinaryGumbelMax.logits(fit: FitResult, parent_values: Array) -> Array

The (n, K) class logits at the posterior mean.

parent_values is the (n, p) raw parent matrix (codes for discrete parents); it is one-hot encoded internally so the weight rows line up.

BinaryGumbelMax.logits_per_draw#

python
def BinaryGumbelMax.logits_per_draw(
    fit: FitResult,
    parent_values: Array,
) -> Array

The (S, n, K) class logits per parameter-posterior draw.

The per-draw twin of logits(): bias is (S, K) and weight (S, P, K), so each draw's logits read bias[d] + encode(PA)[d] . w[d]. parent_values is the (S, n, p) raw per-draw parent matrix (an intervened/categorical parent's singleton draw axis broadcasts up to S, and is one-hot encoded per draw so the design matches the (S, P, K) weight draws). With no parents the result is the broadcast bias draws. Used by the counterfactual to spread a categorical ancestor's decision across draws, so its logit-parameter posterior enters the counterfactual interval (the posterior-mean collapse it replaces understated the width).

BinaryGumbelMax.abduct#

python
def BinaryGumbelMax.abduct(
    fit: FitResult,
    observed_codes: IntArray,
    parent_values: Array,
    *,
    seed: int,
) -> Array

Abduct the per-unit Gumbel noise consistent with the observed category.

BinaryGumbelMax.decide#

python
def BinaryGumbelMax.decide(logits: Array, gumbel: Array) -> IntArray

Return the category per row: argmax(logits + gumbel).

Parameter Description
logits The (n, K) class logits.
gumbel The (n, K) Gumbel noise, sampled or abducted.

Returns: The (n,) integer category codes.

Capability#

python
class Capability(enum.Enum)

What a mechanism's noise lets it answer.

PROBABILISTIC covers observational and interventional queries. INVERTIBLE adds counterfactuals, because the structural noise is recoverable from the node and its parents.

Member Value
PROBABILISTIC 'probabilistic'
INVERTIBLE 'invertible'

CategoricalGumbelMax#

python
class CategoricalGumbelMax(
    name: str,
    parents: tuple[str, ...],
    parent_cards: tuple[int | None, ...] = (),
    n_categories: int = 0,
    coef_prior_sd: float = 3.0,
    latents: tuple[str, ...] = (),
    gauged_latents: tuple[str, ...] = (),
    loading_prior_sd: float = 2.0,
) -> None

Categorical mechanism as Gumbel-max over softmax logits.

The logits are linear in the parents with a per-class slope and intercept, fitted by the equivalent Categorical likelihood (the Gumbel-max marginal). The forward mechanism is category = argmax(logits + g) with g an iid Gumbel vector, and the counterfactual reuses the abducted g under shifted logits, so it is invertible. Identified up to the per-row logit gauge, which the argmax washes out.

Field Type Default
name str required
parents tuple[str, ...] required
parent_cards tuple[int | None, ...] ()
n_categories int 0
coef_prior_sd float 3.0
latents tuple[str, ...] ()
gauged_latents tuple[str, ...] ()
loading_prior_sd float 2.0

CategoricalGumbelMax.capability#

python
property CategoricalGumbelMax.capability: Capability

Always INVERTIBLE: the Gumbel noise can be abducted.

CategoricalGumbelMax.weight_name#

python
property CategoricalGumbelMax.weight_name: str

The posterior parameter name of the per-class weight matrix, {name}__logit_w.

CategoricalGumbelMax.bias_name#

python
property CategoricalGumbelMax.bias_name: str

The posterior parameter name of the per-class bias, {name}__logit_b.

CategoricalGumbelMax.loading_name#

python
def CategoricalGumbelMax.loading_name(latent: str) -> str

The per-class logit loading parameter for this node on latent L.

CategoricalGumbelMax.build#

python
def CategoricalGumbelMax.build(
    x: Array,
    y_codes: IntArray,
    n_cat: int,
) -> Any

The per-node PyMC model: y ~ Categorical(logits = bias + x_encoded . weight).

Parameter Description
x The raw (n, p) parent matrix (codes for discrete parents), one-hot encoded here.
y_codes The observed category codes.
n_cat The number of categories.

Returns: A pymc.Model.

CategoricalGumbelMax.generative_rv#

python
def CategoricalGumbelMax.generative_rv(
    nodes: Mapping[str, Any],
    n: int,
) -> Any

Add this node's categorical RV to the active generative model.

logits = bias + encode(PA) . weight (same per-class linear form and same one-hot parent encoding the fit uses), then pm.Categorical samples the node forward (the Gumbel-max marginal). The RV names match the merged posterior, so sample_posterior_predictive reads the fitted logits. The node's resolved class count must be passed in (it lives on the fit, not the spec). An incident latent L adds a per-class logit term L * loading_L (loading_L shape (K,)), so the confounding shifts the relative class logits, and pm.do severs the node from L.

CategoricalGumbelMax.fit#

python
def CategoricalGumbelMax.fit(
    data: Dataset,
    *,
    draws: int,
    tune: int,
    chains: int,
    seed: int,
) -> FitResult

Fit this node alone by NUTS and return its parameter posterior.

The class count is n_categories when set, else read from the column's categories. A node in a confounded district is fitted jointly by fit(), not here.

Parameter Description
data The dataset holding the node and its parents.
draws Posterior draws per chain.
tune Tuning steps per chain.
chains Number of chains.
seed The sampler seed.

Returns: A FitResult with the bias and (with parents) weight draws, their posterior means, and the resolved class count.

CategoricalGumbelMax.logits#

python
def CategoricalGumbelMax.logits(
    fit: FitResult,
    parent_values: Array,
) -> Array

The (n, K) class logits at the posterior mean.

parent_values is the (n, p) raw parent matrix (codes for discrete parents); it is one-hot encoded internally so the weight rows line up.

CategoricalGumbelMax.logits_per_draw#

python
def CategoricalGumbelMax.logits_per_draw(
    fit: FitResult,
    parent_values: Array,
) -> Array

The (S, n, K) class logits per parameter-posterior draw.

The per-draw twin of logits(): bias is (S, K) and weight (S, P, K), so each draw's logits read bias[d] + encode(PA)[d] . w[d]. parent_values is the (S, n, p) raw per-draw parent matrix (an intervened/categorical parent's singleton draw axis broadcasts up to S, and is one-hot encoded per draw so the design matches the (S, P, K) weight draws). With no parents the result is the broadcast bias draws. Used by the counterfactual to spread a categorical ancestor's decision across draws, so its logit-parameter posterior enters the counterfactual interval (the posterior-mean collapse it replaces understated the width).

CategoricalGumbelMax.abduct#

python
def CategoricalGumbelMax.abduct(
    fit: FitResult,
    observed_codes: IntArray,
    parent_values: Array,
    *,
    seed: int,
) -> Array

Abduct the per-unit Gumbel noise consistent with the observed category.

CategoricalGumbelMax.decide#

python
def CategoricalGumbelMax.decide(logits: Array, gumbel: Array) -> IntArray

Return the category per row: argmax(logits + gumbel).

Parameter Description
logits The (n, K) class logits.
gumbel The (n, K) Gumbel noise, sampled or abducted.

Returns: The (n,) integer category codes.

CategoricalResult#

python
class CategoricalResult(
    query: str,
    target: str,
    categories: tuple[object, ...],
    probabilities: npt.NDArray[np.float64],
    identified: bool,
    diagnostics: dict[str, float] = <factory>,
    _per_unit: npt.NDArray[np.int64] | None = None,
) -> None

A posterior over a categorical query's answer.

A categorical node's interventional or counterfactual answer is a distribution over its categories, not a single number, so this carries the per-category probabilities (probabilities, summing to one over categories) and the per-unit draws when a counterfactual was asked of specific units. identified carries the same honesty flag as SCMResult.

Field Type Default
query str required
target str required
categories tuple[object, ...] required
probabilities npt.NDArray[np.float64] required
identified bool required
diagnostics dict[str, float] factory

CategoricalResult.mode#

python
def CategoricalResult.mode() -> object

The most probable category.

CategoricalResult.per_unit#

python
def CategoricalResult.per_unit() -> npt.NDArray[np.int64] | None

Per-unit counterfactual category codes, when units were queried.

GaussianAdditive#

python
class GaussianAdditive(
    name: str,
    parents: tuple[str, ...],
    parent_cards: tuple[int | None, ...] = (),
    coef_prior_sd: float = 5.0,
    sigma_prior_sd: float = 2.0,
    latents: tuple[str, ...] = (),
    gauged_latents: tuple[str, ...] = (),
    loading_prior_sd: float = 2.0,
) -> None

Additive-noise Gaussian mechanism, X = b0 + w . PA + U, U ~ N(0, sigma).

The default continuous mechanism: a Bayesian-linear mean over the parents with a half-normal prior on the noise scale. Invertible, so its counterfactual is the exact residual U = X - (b0 + w . PA) reused under the action. A Learner-as-mean override (carrying no parameter posterior, abducting via the residual against the fitted mean) is not available; the mean is always the Bayesian-linear one.

In a confounded district the node may also carry incident latents: one per-unit L ~ Normal(0, 1) per bidirected edge the node touches, entering the mean as + loading_L * L. latents lists those latent RV names (in a deterministic order); gauged_latents is the subset whose loading is pinned positive (a HalfNormal) to kill the L -> -L reflection. A Markovian node leaves both empty and reduces to the plain additive mechanism exactly.

Field Type Default
name str required
parents tuple[str, ...] required
parent_cards tuple[int | None, ...] ()
coef_prior_sd float 5.0
sigma_prior_sd float 2.0
latents tuple[str, ...] ()
gauged_latents tuple[str, ...] ()
loading_prior_sd float 2.0

GaussianAdditive.capability#

python
property GaussianAdditive.capability: Capability

Always INVERTIBLE: the residual is recoverable.

GaussianAdditive.coef_name#

python
property GaussianAdditive.coef_name: str

The posterior parameter name of the slope vector, {name}__coef.

GaussianAdditive.intercept_name#

python
property GaussianAdditive.intercept_name: str

The posterior parameter name of the intercept, {name}__intercept.

GaussianAdditive.sigma_name#

python
property GaussianAdditive.sigma_name: str

The posterior parameter name of the noise scale, {name}__sigma.

GaussianAdditive.loading_name#

python
def GaussianAdditive.loading_name(latent: str) -> str

The parameter name for this node's loading on latent L (district fit).

GaussianAdditive.build#

python
def GaussianAdditive.build(x: Array, y: Array) -> Any

The per-node PyMC model: y ~ Normal(b0 + x_encoded . w, sigma).

x is the raw parent matrix (codes for discrete parents); it is encoded to the one-hot design here, the same encoding the generative model uses.

GaussianAdditive.generative_rv#

python
def GaussianAdditive.generative_rv(nodes: Mapping[str, Any], n: int) -> Any

Add this node's RV to the active generative model: Normal(b0 + PA . w, sigma).

nodes maps each (already-built) parent name to its RV. Parents are encoded by the shared pytensor encoder, so the design matches the fit view column for column; the RV names match the merged posterior exactly, so sample_posterior_predictive reads the fitted draws. An incident latent L (a per-unit Normal(0, 1) the compose helper builds first, found in nodes) adds loading_L * L to the mean, so pm.do on this node severs it from L while a district sibling still reads L -- the confounding cancels in an interventional contrast.

GaussianAdditive.fit#

python
def GaussianAdditive.fit(
    data: Dataset,
    *,
    draws: int,
    tune: int,
    chains: int,
    seed: int,
) -> FitResult

Fit this node alone by NUTS and return its parameter posterior.

Reads the node's column and its parents' columns from data. A node in a confounded district is fitted jointly by fit(), not here.

Parameter Description
data The dataset holding the node and its parents.
draws Posterior draws per chain.
tune Tuning steps per chain.
chains Number of chains.
seed The sampler seed.

Returns: A FitResult with the intercept, noise scale and (with parents) slope draws, plus their posterior means.

GaussianAdditive.structural_mean#

python
def GaussianAdditive.structural_mean(
    fit: FitResult,
    parent_values: Array,
) -> Array

The mean b0 + w . encode(PA) at the posterior mean.

parent_values is the (n, p) raw parent matrix (codes for discrete parents); it is one-hot encoded internally, so the coefficients line up with the fit. (n, p) in, (n,) out.

GaussianAdditive.mean_per_draw#

python
def GaussianAdditive.mean_per_draw(
    fit: FitResult,
    parent_values: Array,
) -> Array

The mean per parameter-posterior draw.

parent_values is the (S, n, p) raw per-draw parent matrix (a non-intervened parent carries its own counterfactual posterior; codes for a discrete parent). It is one-hot encoded per draw, so the encoded design matches the (S, P) coefficient draws. Returns (S, n). With no parents the result is the broadcast intercept draws.

GaussianAdditive.sigma#

python
def GaussianAdditive.sigma(fit: FitResult, *, use_draws: bool) -> Array

Return the noise scale: its (S,) draws, or the posterior mean.

Parameter Description
fit The node's fitted posterior.
use_draws Return the draws when True, the posterior mean when False.

GaussianAdditive.abduct#

python
def GaussianAdditive.abduct(
    fit: FitResult,
    value: Array,
    parent_values: Array,
) -> Array

The exact residual U = X - (b0 + w . PA) at the posterior mean.

InterventionView#

python
class InterventionView(scm: SCM, intervention: Mapping[str, DoValue]) -> None

A lightweight intervened view of a fitted SCM, for chaining.

Returned by do(). It carries the intervention and forwards interventional()/sample() with the do-map applied, sharing the parent's fit. Immutable; do returns a new view.

InterventionView.do#

python
def InterventionView.do(more: Mapping[str, DoValue]) -> InterventionView

Return a new view with more added to the intervention; later values win per node.

InterventionView.interventional#

python
def InterventionView.interventional(
    target: str,
    *,
    n: int = 2000,
) -> SCMResult | CategoricalResult

Return the distribution of target under the view's intervention.

The same as interventional() with the view's do-map.

Parameter Description
target The node to read.
n Number of simulated units.

InterventionView.sample#

python
def InterventionView.sample(
    target: str,
    *,
    n: int = 2000,
) -> SCMResult | CategoricalResult

Alias for the interventional distribution of target under the view.

Mechanism#

python
class Mechanism(*args, **kwargs)

The per-node structural equation.

Implementations carry the node name and its parent names and expose the four jobs below. The SCM holds one mechanism per node and a FitResult per mechanism after fitting. name and parents are read-only: mechanisms are frozen value objects, so filling a blank node name returns a new instance (resolve_mechanism()) rather than mutating in place.

Mechanism.name#

python
property Mechanism.name: str

The node this mechanism is the structural equation for.

Mechanism.parents#

python
property Mechanism.parents: tuple[str, ...]

The node's observed parents, in the mechanism's design order.

Mechanism.latents#

python
property Mechanism.latents: tuple[str, ...]

Incident latent RV names from confounded-district loadings; () for a Markovian node.

Mechanism.gauged_latents#

python
property Mechanism.gauged_latents: tuple[str, ...]

The subset of latents whose loading is pinned positive for the sign gauge.

Mechanism.capability#

python
property Mechanism.capability: Capability

INVERTIBLE if counterfactuals are exact, else PROBABILISTIC.

Mechanism.fit#

python
def Mechanism.fit(
    data: Dataset,
    *,
    draws: int,
    tune: int,
    chains: int,
    seed: int,
) -> FitResult

Fit the node on its observed parents; return the posterior.

Mechanism.generative_rv#

python
def Mechanism.generative_rv(nodes: Mapping[str, Any], n: int) -> Any

Add this node's RV to the active generative PyMC model, return the RV.

nodes maps each already-built parent name to its RV; the structural equation reads the parents from it. Names every parameter RV with the same scheme fit() used, so the merged posterior matches by name. Called inside a pm.Model() context by the compose helper.

SCMResult#

python
class SCMResult(
    query: str,
    target: str,
    kind: str,
    identified: bool,
    diagnostics: dict[str, float] = <factory>,
    _draws: npt.NDArray[np.float64] = <factory>,
) -> None

Draws of a query's answer.

query names what was asked ("sample", "interventional", "counterfactual", "ate"). target names the node read. _draws holds the draws (flattened); the accessors read them. identified records whether the answer is a function of the data alone.

kind says what the draws are. "posterior" (ate, counterfactual): draws of the answer itself, so interval is a credible interval. "predictive" (sample, marginal, interventional): posterior-predictive draws of the outcome, one unit each, so value estimates the mean outcome while std_error and interval give the spread of the outcome, not the uncertainty in its mean.

Field Type Default
query str required
target str required
kind str required
identified bool required
diagnostics dict[str, float] factory

SCMResult.value#

python
property SCMResult.value: float

The mean of the draws (the point summary).

SCMResult.mean#

python
def SCMResult.mean() -> float

The posterior mean of the answer.

SCMResult.std_error#

python
property SCMResult.std_error: float | None

The standard deviation of the draws (the outcome's spread when predictive).

SCMResult.draws#

python
def SCMResult.draws() -> npt.NDArray[np.float64]

The posterior draws of the answer.

SCMResult.interval#

python
def SCMResult.interval(level: float = 0.95) -> tuple[float, float]

An interval at level from the draw quantiles.

A credible interval for a "posterior" result; for a "predictive" result, the range a single unit's outcome falls in.

Sensitivity#

python
class Sensitivity(
    treatment: str,
    outcome: str,
    confounding: npt.NDArray[np.float64],
    effect: npt.NDArray[np.float64],
    identified_at_zero: float,
    obstruction: str,
    diagnostics: dict[str, float] = <factory>,
) -> None

The effect as a function of the assumed confounding strength.

Returned in place of a point when an effect is not identified. confounding is the grid of assumed confounding-channel values c (a covariance on the treatment-outcome pair), effect the implied causal effect b(c) at each. identified_at_zero is b(0), the answer under the no-confounding assumption (the observational slope), which is where the curve meets the identified region. obstruction is the human-readable reason (the hedge).

There is no scalar accessor on purpose: a non-identified effect is a range, not a number. Read range(), at(), or the arrays.

Field Type Default
treatment str required
outcome str required
confounding npt.NDArray[np.float64] required
effect npt.NDArray[np.float64] required
identified_at_zero float required
obstruction str required
diagnostics dict[str, float] factory

Sensitivity.identified#

python
property Sensitivity.identified: bool

Always False: a Sensitivity exists because the effect is not.

Sensitivity.range#

python
def Sensitivity.range() -> tuple[float, float]

The (min, max) implied effect over the swept confounding grid.

Sensitivity.at#

python
def Sensitivity.at(c: float) -> float

The implied causal effect if the confounding channel strength is c.

Linear interpolation on the swept grid; c outside the grid extrapolates along the line, which is exact for the linear-Gaussian case.

Suggest an edit
Documentation

Search ergodic 0.2.0

Search ergodic by task, function or error.