antecedent

Package type stub for causal.

Day-1: analyze, queries, Dag/graphs, identify, fit_gcm, Identifier/Estimator, inference modes, AnalysisResult. Stage depth: causal.discovery, causal.gcm, causal.graph, etc. AteAnalysisResult is private to causal._native.

  1"""causal — Python bindings for the causal-library Rust workspace.
  2
  3Day-1 surface::
  4
  5    result = causal.analyze(
  6        data,
  7        graph=edges,
  8        query=causal.AverageEffect(treatment="t", outcome="y"),
  9    )
 10
 11Stage modules hold depth: ``causal.discovery``, ``causal.estimation``,
 12``causal.gcm``, ``causal.graph``, ``causal.attribution``, ``causal.validation``.
 13Prefer ``Dag.from_dot`` / ``.to_dot`` (and peers); free ``dag_from_*`` helpers
 14remain thin aliases under ``causal`` and ``causal.graph``.
 15
 16Public analysis results are nested ``AnalysisResult`` views. The native DTO
 17``AteAnalysisResult`` lives on ``causal._native`` only.
 18
 19The native extension ``causal._native`` is an advanced FFI surface.
 20"""
 21
 22from __future__ import annotations
 23
 24from . import (
 25    attribution,
 26    counterfactual,
 27    data,
 28    design,
 29    discovery,
 30    estimation,
 31    extensibility,
 32    gcm,
 33    graph,
 34    inference,
 35    model,
 36    population,
 37    query,
 38    state,
 39    validation,
 40)
 41from .data import EventFrame, MultiEnvFrame, PanelFrame, event, multi_env, panel
 42from .design import evaluate_decision
 43from ._native import (
 44    Admg,
 45    AnomalyScores,
 46    ArrowLoadInfo,
 47    CausalAttributionError,
 48    CausalCompileError,
 49    CausalCounterfactualError,
 50    CausalDataError,
 51    CausalDesignError,
 52    CausalDiscoveryError,
 53    CausalError,
 54    CausalEstimateError,
 55    CausalGraphError,
 56    CausalIdentifyError,
 57    CausalModelError,
 58    CausalResourceError,
 59    CausalReviewError,
 60    CausalSerializationError,
 61    CausalState,
 62    CausalStateError,
 63    CausalUnsupportedError,
 64    CausalCancelledError,
 65    CancellationToken,
 66    CausalValidateError,
 67    ChangeAttributionResult,
 68    Contribution,
 69    Cpdag,
 70    Dag,
 71    DecisionEvaluation,
 72    DesignRanking,
 73    DiscoveredLink,
 74    FeatureRelevance,
 75    FittedGcm,
 76    GcmIteResult,
 77    GcmSampleResult,
 78    GraphEdge,
 79    GraphPosterior,
 80    MechanismChangeDetection,
 81    MediationEffectsSummary,
 82    Pag,
 83    PcmciDiscoveryResult,
 84    PosteriorArtifact,
 85    PredictSummary,
 86    RpcmciDiscoverySummary,
 87    TemporalCpdag,
 88    TemporalDag,
 89    TemporalPag,
 90    anomaly_attribution,
 91    attribute_distribution_change,
 92    attribute_distribution_change_robust,
 93    attribute_feature_relevance,
 94    attribute_path_specific,
 95    attribute_paths,
 96    attribute_structure_change,
 97    attribute_unit_change,
 98    counterfactual_ite,
 99    dag_from_dot,
100    dag_from_gml,
101    dag_from_json,
102    dag_from_networkx_adjacency,
103    dag_from_networkx_node_link,
104    dag_to_dot,
105    dag_to_gml,
106    dag_to_json,
107    dag_to_networkx_adjacency,
108    dag_to_networkx_node_link,
109    decode_model_bundle,
110    decode_posterior_artifact,
111    encode_model_bundle,
112    encode_posterior_artifact,
113    fit_gcm,
114    load_float64_arrow_c_columns,
115    load_float64_columns,
116    mechanism_change_detection,
117    mediation_effects_summary,
118    predict_intervened_summary,
119    rank_designs,
120    rank_root_causes,
121    sample_do,
122    sample_interventional_distribution,
123)
124from .gcm import (
125    anomaly_attribution_discovered,
126    attribute_distribution_change_discovered,
127    attribute_paths_discovered,
128    fit_gcm_discovered,
129)
130from .population import (
131    PopulationRegistry,
132    target_all,
133    target_custom_distribution,
134    target_named,
135    target_rows,
136    target_treated,
137    target_untreated,
138)
139from .discovery import (
140    CiScreenedPosterior,
141    DbnPosterior,
142    ExactDagPosterior,
143    FCI,
144    GES,
145    JPCMCIPlus,
146    LPCMCI,
147    LiNGAM,
148    NOTEARS,
149    OrderMcmc,
150    PC,
151    PCMCI,
152    PCMCIPlus,
153    RFCI,
154    RPCMCI,
155    StructureMcmc,
156    discover_ci_screened_posterior,
157    discover_dbn_posterior,
158    discover_exact_dag_posterior,
159    discover_jpcmci_plus,
160    discover_lpcmci,
161    discover_order_mcmc,
162    discover_pc,
163    discover_ges,
164    discover_lingam,
165    discover_notears,
166    discover_fci,
167    discover_rfci,
168    discover_pcmci,
169    discover_pcmci_plus,
170    discover_rpcmci,
171    discover_structure_mcmc,
172    two_regime_half_split,
173)
174from .estimation import (
175    AnalysisResult,
176    ConflictSummaryView,
177    EffectEnvelope,
178    PredictiveCheckReport,
179    PreparedAnalysis,
180    PriorSensitivityReport,
181    IdentifyResult,
182    analyze,
183    analyze_many,
184    identify,
185)
186from .accepted_graph import AcceptedGraph
187from .inference import Bayesian, Frequentist
188from .ids import Estimator, Identifier, Latency, Refute
189from .prior_bank import (
190    CompatibilityReport,
191    ComposedPrior,
192    ConflictPolicy,
193    DesignVariable,
194    EstimandFingerprint,
195    ExternalPriorSourceSpec,
196    ExternalPriorWeight,
197    POPULATION_TAG_KEY,
198    PriorCatalog,
199    PriorMapping,
200    PriorSource,
201    PriorSourceMeta,
202    TransportPolicy,
203    compose_external_priors,
204    populations_from_prior_sources,
205)
206from .query import (
207    AverageEffect,
208    ConditionalEffect,
209    Counterfactual,
210    InterventionalDistribution,
211    MediationEffect,
212    PathSpecificEffect,
213    PulseEffect,
214    SustainedEffect,
215    TemporalMediationEffect,
216)
217from .validation import (
218    validate_environment_holdout,
219    validate_pcmci_alpha_sensitivity,
220    validate_pcmci_block_bootstrap,
221    validate_pcmci_ci_sensitivity,
222    validate_pcmci_false_positive,
223    validate_pcmci_lag_sensitivity,
224    validate_pcmci_plus_orientation,
225    validate_regime_stability,
226    validate_synthetic_null_calibration,
227)
228
229__all__ = [
230    # Day-1
231    "AcceptedGraph",
232    "Admg",
233    "AnalysisResult",
234    "AverageEffect",
235    "Bayesian",
236    "ConditionalEffect",
237    "Counterfactual",
238    "Cpdag",
239    "Dag",
240    "Estimator",
241    "Frequentist",
242    "Identifier",
243    "Latency",
244    "Refute",
245    "IdentifyResult",
246    "InterventionalDistribution",
247    "MediationEffect",
248    "Pag",
249    "PathSpecificEffect",
250    "PreparedAnalysis",
251    "PulseEffect",
252    "SustainedEffect",
253    "TemporalDag",
254    "TemporalMediationEffect",
255    "analyze",
256    "analyze_many",
257    "identify",
258    "fit_gcm",
259    "FittedGcm",
260    # Errors
261    "CausalAttributionError",
262    "CausalCompileError",
263    "CausalCounterfactualError",
264    "CausalDataError",
265    "CausalDesignError",
266    "CausalDiscoveryError",
267    "CausalError",
268    "CausalEstimateError",
269    "CausalGraphError",
270    "CausalIdentifyError",
271    "CausalModelError",
272    "CausalResourceError",
273    "CausalReviewError",
274    "CausalSerializationError",
275    "CausalStateError",
276    "CausalUnsupportedError",
277    "CausalCancelledError",
278    "CancellationToken",
279    "CausalValidateError",
280    # Stage re-exports (prefer causal.discovery / .gcm / .graph / …)
281    "AnomalyScores",
282    "ArrowLoadInfo",
283    "ChangeAttributionResult",
284    "CompatibilityReport",
285    "CiScreenedPosterior",
286    "Contribution",
287    "DbnPosterior",
288    "DecisionEvaluation",
289    "DesignRanking",
290    "DesignVariable",
291    "DiscoveredLink",
292    "ExactDagPosterior",
293    "EstimandFingerprint",
294    "FCI",
295    "FeatureRelevance",
296    "GES",
297    "GcmIteResult",
298    "GcmSampleResult",
299    "GraphEdge",
300    "GraphPosterior",
301    "JPCMCIPlus",
302    "LPCMCI",
303    "LiNGAM",
304    "MechanismChangeDetection",
305    "MediationEffectsSummary",
306    "NOTEARS",
307    "OrderMcmc",
308    "PC",
309    "PCMCI",
310    "PCMCIPlus",
311    "PcmciDiscoveryResult",
312    "PopulationRegistry",
313    "PosteriorArtifact",
314    "PredictSummary",
315    "EffectEnvelope",
316    "PredictiveCheckReport",
317    "ConflictSummaryView",
318    "PriorCatalog",
319    "PriorMapping",
320    "PriorSensitivityReport",
321    "PriorSource",
322    "PriorSourceMeta",
323    "ComposedPrior",
324    "ConflictPolicy",
325    "TransportPolicy",
326    "ExternalPriorSourceSpec",
327    "ExternalPriorWeight",
328    "POPULATION_TAG_KEY",
329    "compose_external_priors",
330    "populations_from_prior_sources",
331    "RFCI",
332    "RPCMCI",
333    "RpcmciDiscoverySummary",
334    "StructureMcmc",
335    "TemporalCpdag",
336    "TemporalPag",
337    "CausalState",
338    "anomaly_attribution",
339    "attribute_distribution_change",
340    "attribute_distribution_change_robust",
341    "attribute_feature_relevance",
342    "attribute_path_specific",
343    "attribute_paths",
344    "attribute_structure_change",
345    "attribute_unit_change",
346    "attribution",
347    "counterfactual",
348    "counterfactual_ite",
349    "dag_from_dot",
350    "dag_from_gml",
351    "dag_from_json",
352    "dag_from_networkx_adjacency",
353    "dag_from_networkx_node_link",
354    "dag_to_dot",
355    "dag_to_gml",
356    "dag_to_json",
357    "dag_to_networkx_adjacency",
358    "dag_to_networkx_node_link",
359    "data",
360    "decode_model_bundle",
361    "decode_posterior_artifact",
362    "design",
363    "discover_ci_screened_posterior",
364    "discover_dbn_posterior",
365    "discover_exact_dag_posterior",
366    "discover_jpcmci_plus",
367    "discover_lpcmci",
368    "discover_order_mcmc",
369    "discover_pc",
370    "discover_ges",
371    "discover_lingam",
372    "discover_notears",
373    "discover_fci",
374    "discover_rfci",
375    "discover_pcmci",
376    "discover_pcmci_plus",
377    "discover_rpcmci",
378    "discover_structure_mcmc",
379    "discovery",
380    "two_regime_half_split",
381    "encode_model_bundle",
382    "encode_posterior_artifact",
383    "estimation",
384    "evaluate_decision",
385    "event",
386    "EventFrame",
387    "extensibility",
388    "fit_gcm_discovered",
389    "gcm",
390    "graph",
391    "inference",
392    "load_float64_arrow_c_columns",
393    "load_float64_columns",
394    "mechanism_change_detection",
395    "mediation_effects_summary",
396    "model",
397    "multi_env",
398    "MultiEnvFrame",
399    "panel",
400    "PanelFrame",
401    "population",
402    "predict_intervened_summary",
403    "query",
404    "rank_designs",
405    "rank_root_causes",
406    "sample_do",
407    "sample_interventional_distribution",
408    "state",
409    "target_all",
410    "target_custom_distribution",
411    "target_named",
412    "target_rows",
413    "target_treated",
414    "target_untreated",
415    "attribute_paths_discovered",
416    "anomaly_attribution_discovered",
417    "attribute_distribution_change_discovered",
418    "validate_environment_holdout",
419    "validate_pcmci_alpha_sensitivity",
420    "validate_pcmci_block_bootstrap",
421    "validate_pcmci_ci_sensitivity",
422    "validate_pcmci_false_positive",
423    "validate_pcmci_lag_sensitivity",
424    "validate_pcmci_plus_orientation",
425    "validate_regime_stability",
426    "validate_synthetic_null_calibration",
427    "validation",
428    "__version__",
429]
430
431try:
432    from ._native import __version__ as __version__
433except ImportError:  # pragma: no cover - extension not built
434    __version__ = "0.2.0"
class AcceptedGraph:
240class AcceptedGraph:
241    """Versioned accepted CPDAG/PAG/DAG/temporal completion for estimate-only clicks.
242
243    Estimate / prepare / refresh never call discovery. Only :meth:`rediscover`
244    or constructing a new handle may replace structure and bump :attr:`version`.
245    """
246
247    __slots__ = ("_graph", "_version", "_algorithm_id")
248
249    def __init__(
250        self,
251        graph: _GraphTypes,
252        *,
253        version: int = 1,
254        algorithm_id: str | None = None,
255    ) -> None:
256        if version < 1:
257            raise ValueError("version must be >= 1")
258        self._graph = graph
259        self._version = int(version)
260        self._algorithm_id = algorithm_id
261
262    @property
263    def graph(self) -> _GraphTypes:
264        return self._graph
265
266    @property
267    def version(self) -> int:
268        return self._version
269
270    @property
271    def algorithm_id(self) -> str | None:
272        return self._algorithm_id
273
274    @classmethod
275    def from_graph(
276        cls,
277        graph: _GraphTypes,
278        *,
279        algorithm_id: str | None = None,
280        version: int = 1,
281    ) -> AcceptedGraph:
282        """Hold a reviewed or hand-authored graph artifact."""
283        return cls(graph, version=version, algorithm_id=algorithm_id)
284
285    @classmethod
286    def from_discovery(
287        cls,
288        result: DiscoveryResult,
289        *,
290        algorithm_id: str,
291        version: int = 1,
292    ) -> AcceptedGraph:
293        """Accept a standalone ``discover_*`` result into a session artifact."""
294        if not algorithm_id:
295            raise ValueError("algorithm_id is required for discovery provenance")
296        algo = algorithm_id.lower().replace("pcmci_plus", "pcmci+")
297        if algo in ("pcmci", "pcmci+", "lpcmci"):
298            graph: _GraphTypes = _result_to_temporal_graph(result, algo)
299        else:
300            graph = _result_to_graph(result, algo)
301        return cls(graph, version=version, algorithm_id=algo)
302
303    def replace(
304        self,
305        graph: _GraphTypes,
306        *,
307        algorithm_id: str | None = None,
308    ) -> AcceptedGraph:
309        """Explicit structure replace (bumps version). Returns a new handle."""
310        return AcceptedGraph(
311            graph,
312            version=self._version + 1,
313            algorithm_id=algorithm_id if algorithm_id is not None else self._algorithm_id,
314        )
315
316    def rediscover(
317        self,
318        data: Any,
319        discovery: _AnyDiscovery,
320        *,
321        seed: int = 1,
322        threads: int = 1,
323    ) -> AcceptedGraph:
324        """User-triggered rediscovery; never called by estimate / prepare."""
325        if isinstance(discovery, (PCMCI, PCMCIPlus, LPCMCI)):
326            result, algo = _run_temporal_discovery(
327                data, discovery, seed=seed, threads=threads
328            )
329            graph: _GraphTypes = _result_to_temporal_graph(result, algo)
330        else:
331            result, algo = _run_static_discovery(
332                data, discovery, seed=seed, threads=threads
333            )
334            graph = _result_to_graph(result, algo)
335        return AcceptedGraph(graph, version=self._version + 1, algorithm_id=algo)
336
337    def analyze(self, data: Any, query: Any, **kwargs: Any) -> Any:
338        """Estimate on the held graph (default ``latency="interactive"``).
339
340        Rejects caller ``discovery=``. Does not bump :attr:`version`.
341        """
342        from .estimation import analyze
343
344        if "discovery" in kwargs and kwargs["discovery"] is not None:
345            raise CausalUnsupportedError(
346                "AcceptedGraph.analyze rejects discovery=; structure is already accepted "
347                "(call rediscover() for an explicit structure refresh)"
348            )
349        kwargs.pop("discovery", None)
350        kwargs.setdefault("latency", "interactive")
351        kwargs["graph"] = self._graph
352        return analyze(data, query=query, **kwargs)
353
354    def prepare(self, data: Any, *, query: Any, **kwargs: Any) -> Any:
355        """Compile-once prepared handle on the held static DAG/edges."""
356        from .estimation import PreparedAnalysis
357
358        if isinstance(self._graph, (Cpdag, Pag, Admg, TemporalCpdag, TemporalPag)):
359            raise CausalUnsupportedError(
360                "PreparedAnalysis requires a fully oriented Dag/TemporalDag (or edge list); "
361                "complete CPDAG/PAG review first, then AcceptedGraph.from_graph(...)"
362            )
363        kwargs.setdefault("latency", "interactive")
364        return PreparedAnalysis.prepare(data, query=query, graph=self._graph, **kwargs)  # type: ignore[arg-type]
365
366    def to_json(self) -> str:
367        """Serialize for durable hold (JSON interchange, not CBOR wire)."""
368        kind, payload = _encode_graph(self._graph)
369        return json.dumps(
370            {
371                "format": "causal.AcceptedGraph/v1",
372                "version": self._version,
373                "algorithm_id": self._algorithm_id,
374                "kind": kind,
375                "payload": payload,
376            },
377            separators=(",", ":"),
378        )
379
380    @classmethod
381    def from_json(cls, s: str) -> AcceptedGraph:
382        """Restore from :meth:`to_json`."""
383        obj = json.loads(s)
384        if obj.get("format") != "causal.AcceptedGraph/v1":
385            raise ValueError(f"unsupported AcceptedGraph format: {obj.get('format')!r}")
386        graph = _decode_graph(obj["kind"], obj["payload"])
387        return cls(
388            graph,
389            version=int(obj["version"]),
390            algorithm_id=obj.get("algorithm_id"),
391        )

Versioned accepted CPDAG/PAG/DAG/temporal completion for estimate-only clicks.

Estimate / prepare / refresh never call discovery. Only rediscover() or constructing a new handle may replace structure and bump version.

AcceptedGraph( graph: Union[Dag, Cpdag, Pag, Admg, TemporalDag, TemporalCpdag, TemporalPag, Sequence[tuple[str, str]], Sequence[tuple[str, int, str, int]]], *, version: int = 1, algorithm_id: str | None = None)
249    def __init__(
250        self,
251        graph: _GraphTypes,
252        *,
253        version: int = 1,
254        algorithm_id: str | None = None,
255    ) -> None:
256        if version < 1:
257            raise ValueError("version must be >= 1")
258        self._graph = graph
259        self._version = int(version)
260        self._algorithm_id = algorithm_id
graph: Union[Dag, Cpdag, Pag, Admg, TemporalDag, TemporalCpdag, TemporalPag, Sequence[tuple[str, str]], Sequence[tuple[str, int, str, int]]]
262    @property
263    def graph(self) -> _GraphTypes:
264        return self._graph
version: int
266    @property
267    def version(self) -> int:
268        return self._version
algorithm_id: str | None
270    @property
271    def algorithm_id(self) -> str | None:
272        return self._algorithm_id
@classmethod
def from_graph( cls, graph: Union[Dag, Cpdag, Pag, Admg, TemporalDag, TemporalCpdag, TemporalPag, Sequence[tuple[str, str]], Sequence[tuple[str, int, str, int]]], *, algorithm_id: str | None = None, version: int = 1) -> AcceptedGraph:
274    @classmethod
275    def from_graph(
276        cls,
277        graph: _GraphTypes,
278        *,
279        algorithm_id: str | None = None,
280        version: int = 1,
281    ) -> AcceptedGraph:
282        """Hold a reviewed or hand-authored graph artifact."""
283        return cls(graph, version=version, algorithm_id=algorithm_id)

Hold a reviewed or hand-authored graph artifact.

@classmethod
def from_discovery( cls, result: PcmciDiscoveryResult, *, algorithm_id: str, version: int = 1) -> AcceptedGraph:
285    @classmethod
286    def from_discovery(
287        cls,
288        result: DiscoveryResult,
289        *,
290        algorithm_id: str,
291        version: int = 1,
292    ) -> AcceptedGraph:
293        """Accept a standalone ``discover_*`` result into a session artifact."""
294        if not algorithm_id:
295            raise ValueError("algorithm_id is required for discovery provenance")
296        algo = algorithm_id.lower().replace("pcmci_plus", "pcmci+")
297        if algo in ("pcmci", "pcmci+", "lpcmci"):
298            graph: _GraphTypes = _result_to_temporal_graph(result, algo)
299        else:
300            graph = _result_to_graph(result, algo)
301        return cls(graph, version=version, algorithm_id=algo)

Accept a standalone discover_* result into a session artifact.

def replace( self, graph: Union[Dag, Cpdag, Pag, Admg, TemporalDag, TemporalCpdag, TemporalPag, Sequence[tuple[str, str]], Sequence[tuple[str, int, str, int]]], *, algorithm_id: str | None = None) -> AcceptedGraph:
303    def replace(
304        self,
305        graph: _GraphTypes,
306        *,
307        algorithm_id: str | None = None,
308    ) -> AcceptedGraph:
309        """Explicit structure replace (bumps version). Returns a new handle."""
310        return AcceptedGraph(
311            graph,
312            version=self._version + 1,
313            algorithm_id=algorithm_id if algorithm_id is not None else self._algorithm_id,
314        )

Explicit structure replace (bumps version). Returns a new handle.

def rediscover( self, data: Any, discovery: PC | GES | LiNGAM | NOTEARS | FCI | RFCI | PCMCI | PCMCIPlus | LPCMCI, *, seed: int = 1, threads: int = 1) -> AcceptedGraph:
316    def rediscover(
317        self,
318        data: Any,
319        discovery: _AnyDiscovery,
320        *,
321        seed: int = 1,
322        threads: int = 1,
323    ) -> AcceptedGraph:
324        """User-triggered rediscovery; never called by estimate / prepare."""
325        if isinstance(discovery, (PCMCI, PCMCIPlus, LPCMCI)):
326            result, algo = _run_temporal_discovery(
327                data, discovery, seed=seed, threads=threads
328            )
329            graph: _GraphTypes = _result_to_temporal_graph(result, algo)
330        else:
331            result, algo = _run_static_discovery(
332                data, discovery, seed=seed, threads=threads
333            )
334            graph = _result_to_graph(result, algo)
335        return AcceptedGraph(graph, version=self._version + 1, algorithm_id=algo)

User-triggered rediscovery; never called by estimate / prepare.

def analyze(self, data: Any, query: Any, **kwargs: Any) -> Any:
337    def analyze(self, data: Any, query: Any, **kwargs: Any) -> Any:
338        """Estimate on the held graph (default ``latency="interactive"``).
339
340        Rejects caller ``discovery=``. Does not bump :attr:`version`.
341        """
342        from .estimation import analyze
343
344        if "discovery" in kwargs and kwargs["discovery"] is not None:
345            raise CausalUnsupportedError(
346                "AcceptedGraph.analyze rejects discovery=; structure is already accepted "
347                "(call rediscover() for an explicit structure refresh)"
348            )
349        kwargs.pop("discovery", None)
350        kwargs.setdefault("latency", "interactive")
351        kwargs["graph"] = self._graph
352        return analyze(data, query=query, **kwargs)

Estimate on the held graph (default latency="interactive").

Rejects caller discovery=. Does not bump version.

def prepare(self, data: Any, *, query: Any, **kwargs: Any) -> Any:
354    def prepare(self, data: Any, *, query: Any, **kwargs: Any) -> Any:
355        """Compile-once prepared handle on the held static DAG/edges."""
356        from .estimation import PreparedAnalysis
357
358        if isinstance(self._graph, (Cpdag, Pag, Admg, TemporalCpdag, TemporalPag)):
359            raise CausalUnsupportedError(
360                "PreparedAnalysis requires a fully oriented Dag/TemporalDag (or edge list); "
361                "complete CPDAG/PAG review first, then AcceptedGraph.from_graph(...)"
362            )
363        kwargs.setdefault("latency", "interactive")
364        return PreparedAnalysis.prepare(data, query=query, graph=self._graph, **kwargs)  # type: ignore[arg-type]

Compile-once prepared handle on the held static DAG/edges.

def to_json(self) -> str:
366    def to_json(self) -> str:
367        """Serialize for durable hold (JSON interchange, not CBOR wire)."""
368        kind, payload = _encode_graph(self._graph)
369        return json.dumps(
370            {
371                "format": "causal.AcceptedGraph/v1",
372                "version": self._version,
373                "algorithm_id": self._algorithm_id,
374                "kind": kind,
375                "payload": payload,
376            },
377            separators=(",", ":"),
378        )

Serialize for durable hold (JSON interchange, not CBOR wire).

@classmethod
def from_json(cls, s: str) -> AcceptedGraph:
380    @classmethod
381    def from_json(cls, s: str) -> AcceptedGraph:
382        """Restore from :meth:`to_json`."""
383        obj = json.loads(s)
384        if obj.get("format") != "causal.AcceptedGraph/v1":
385            raise ValueError(f"unsupported AcceptedGraph format: {obj.get('format')!r}")
386        graph = _decode_graph(obj["kind"], obj["payload"])
387        return cls(
388            graph,
389            version=int(obj["version"]),
390            algorithm_id=obj.get("algorithm_id"),
391        )

Restore from to_json().

class Admg:

Named ADMG (directed + bidirected).

def from_edges(cls, /, names, directed, bidirected=None):
def nodes(self, /):
def parents(self, /, name):
def children(self, /, name):
def bidirected_neighbors(self, /, name):
def node_count(self, /):
def from_dot(cls, /, dot):
def to_dot(self, /):
def from_json(cls, /, json):
def to_json(self, /):
def from_gml(cls, /, gml):
def to_gml(self, /):
def m_separated(self, /, x, y, z=None):

Whether x is m-separated from y given z.

@dataclass(frozen=True)
class AnalysisResult:
219@dataclass(frozen=True)
220class AnalysisResult:
221    """Nested analysis result matching the Rust facade sections."""
222
223    identification: IdentificationView
224    estimate: EstimateView
225    posterior: PosteriorView | None
226    validation: ValidationView
227    performance: PerformanceView
228    diagnostics: list[str]
229    provenance: dict[str, Any]
230    mediation: MediationView | None = None
231    plan: PlanView | None = None
232    _raw: Any = None
233    _prepared: Any = None
234
235    @property
236    def effect(self) -> float:
237        """Primary scalar effect (mediation total when present, else estimate ATE/mean)."""
238        if self.mediation is not None and self.mediation.total is not None:
239            return float(self.mediation.total)
240        if self.estimate.mediation is not None and self.estimate.mediation.total is not None:
241            return float(self.estimate.mediation.total)
242        return self.estimate.ate
243
244    @property
245    def ate(self) -> float:
246        """Alias for :attr:`effect` (prefer ``effect`` for non-ATE queries)."""
247        return self.effect
248
249    def refresh(
250        self,
251        data: Mapping[str, Any] | Any,
252        *,
253        seed: int = 1,
254        threads: int = 1,
255    ) -> AnalysisResult:
256        """Re-estimate on new data via the retained prepared handle.
257
258        Only results from :meth:`PreparedAnalysis.estimate` / ``refresh`` support
259        this. One-shot :func:`analyze` results raise ``TypeError``.
260        """
261        if self._prepared is None:
262            raise TypeError(
263                "AnalysisResult.refresh requires a result from PreparedAnalysis; "
264                "use PreparedAnalysis.prepare(...) then estimate/refresh"
265            )
266        return self._prepared.estimate(data, seed=seed, threads=threads)
267
268    def refute(
269        self,
270        data: Mapping[str, Any] | Any,
271        suite: Refute | Literal["placebo", "full", "cheap"] | bool | str = "placebo",
272        *,
273        seed: int = 1,
274        threads: int = 1,
275        cancel: Any | None = None,
276    ) -> AnalysisResult:
277        """Second-click refute via the retained prepared handle."""
278        if self._prepared is None:
279            raise TypeError(
280                "AnalysisResult.refute requires a result from PreparedAnalysis; "
281                "use PreparedAnalysis.prepare(...) then estimate"
282            )
283        if isinstance(suite, Refute):
284            suite = str(suite)
285        return self._prepared.refute(
286            data, suite, seed=seed, threads=threads, cancel=cancel
287        )

Nested analysis result matching the Rust facade sections.

AnalysisResult( identification: antecedent.estimation.IdentificationView, estimate: antecedent.estimation.EstimateView, posterior: antecedent.estimation.PosteriorView | None, validation: antecedent.estimation.ValidationView, performance: antecedent.estimation.PerformanceView, diagnostics: list[str], provenance: dict[str, typing.Any], mediation: antecedent.estimation.MediationView | None = None, plan: antecedent.estimation.PlanView | None = None, _raw: Any = None, _prepared: Any = None)
diagnostics: list[str]
provenance: dict[str, typing.Any]
mediation: antecedent.estimation.MediationView | None = None
plan: antecedent.estimation.PlanView | None = None
effect: float
235    @property
236    def effect(self) -> float:
237        """Primary scalar effect (mediation total when present, else estimate ATE/mean)."""
238        if self.mediation is not None and self.mediation.total is not None:
239            return float(self.mediation.total)
240        if self.estimate.mediation is not None and self.estimate.mediation.total is not None:
241            return float(self.estimate.mediation.total)
242        return self.estimate.ate

Primary scalar effect (mediation total when present, else estimate ATE/mean).

ate: float
244    @property
245    def ate(self) -> float:
246        """Alias for :attr:`effect` (prefer ``effect`` for non-ATE queries)."""
247        return self.effect

Alias for effect (prefer effect for non-ATE queries).

def refresh( self, data: Union[Mapping[str, Any], Any], *, seed: int = 1, threads: int = 1) -> AnalysisResult:
249    def refresh(
250        self,
251        data: Mapping[str, Any] | Any,
252        *,
253        seed: int = 1,
254        threads: int = 1,
255    ) -> AnalysisResult:
256        """Re-estimate on new data via the retained prepared handle.
257
258        Only results from :meth:`PreparedAnalysis.estimate` / ``refresh`` support
259        this. One-shot :func:`analyze` results raise ``TypeError``.
260        """
261        if self._prepared is None:
262            raise TypeError(
263                "AnalysisResult.refresh requires a result from PreparedAnalysis; "
264                "use PreparedAnalysis.prepare(...) then estimate/refresh"
265            )
266        return self._prepared.estimate(data, seed=seed, threads=threads)

Re-estimate on new data via the retained prepared handle.

Only results from PreparedAnalysis.estimate() / refresh support this. One-shot analyze() results raise TypeError.

def refute( self, data: Union[Mapping[str, Any], Any], suite: Union[Refute, Literal['placebo', 'full', 'cheap'], bool, str] = 'placebo', *, seed: int = 1, threads: int = 1, cancel: typing.Any | None = None) -> AnalysisResult:
268    def refute(
269        self,
270        data: Mapping[str, Any] | Any,
271        suite: Refute | Literal["placebo", "full", "cheap"] | bool | str = "placebo",
272        *,
273        seed: int = 1,
274        threads: int = 1,
275        cancel: Any | None = None,
276    ) -> AnalysisResult:
277        """Second-click refute via the retained prepared handle."""
278        if self._prepared is None:
279            raise TypeError(
280                "AnalysisResult.refute requires a result from PreparedAnalysis; "
281                "use PreparedAnalysis.prepare(...) then estimate"
282            )
283        if isinstance(suite, Refute):
284            suite = str(suite)
285        return self._prepared.refute(
286            data, suite, seed=seed, threads=threads, cancel=cancel
287        )

Second-click refute via the retained prepared handle.

@dataclass(frozen=True)
class AverageEffect:
10@dataclass(frozen=True)
11class AverageEffect:
12    """Average treatment effect (static tabular)."""
13
14    treatment: str
15    outcome: str
16    control_level: float = 0.0
17    active_level: float = 1.0
18    target_population: object | None = None
19    kind: Literal["average"] = "average"

Average treatment effect (static tabular).

AverageEffect( treatment: str, outcome: str, control_level: float = 0.0, active_level: float = 1.0, target_population: object | None = None, kind: Literal['average'] = 'average')
treatment: str
outcome: str
control_level: float = 0.0
active_level: float = 1.0
target_population: object | None = None
kind: Literal['average'] = 'average'
@dataclass(frozen=True)
class Bayesian:
20@dataclass(frozen=True)
21class Bayesian:
22    """Bayesian g-computation (Laplace / conjugate / HMC backends).
23
24    Parameters
25    ----------
26    n_draws:
27        Posterior draw count.
28    prior_scale:
29        Isotropic Gaussian coefficient prior scale when ``prior_from`` is unset.
30        Ignored when ``prior_from`` is provided.
31    prior_from:
32        Posterior artifact bytes from a previous ``result.posterior.artifact``,
33        or a ``ComposedPrior`` from ``compose_external_priors``.
34        Artifact hydrate is deferred until the target design is prepared.
35    mapping:
36        How to map an artifact into the target prior. ``None`` auto-selects:
37        identical coefficient subspace when designs match (sequential Bayes),
38        or ``PriorMapping.effect_functional(...)`` when designs differ and the
39        artifact has an effect quantity. Never silent ``coef_i → coef_i`` across
40        heterogeneous designs. Ignored when ``prior_from`` is a ``ComposedPrior``.
41    backend:
42        Inference backend: ``laplace`` (default), ``conjugate``, or ``hmc``.
43    """
44
45    n_draws: int = 1000
46    prior_scale: float = 10.0
47    prior_from: bytes | ComposedPrior | None = None
48    mapping: PriorMapping | None = None
49    backend: Literal["laplace", "conjugate", "hmc"] = "laplace"
50    kind: Literal["bayesian"] = "bayesian"

Bayesian g-computation (Laplace / conjugate / HMC backends).

Parameters

n_draws: Posterior draw count. prior_scale: Isotropic Gaussian coefficient prior scale when prior_from is unset. Ignored when prior_from is provided. prior_from: Posterior artifact bytes from a previous result.posterior.artifact, or a ComposedPrior from compose_external_priors. Artifact hydrate is deferred until the target design is prepared. mapping: How to map an artifact into the target prior. None auto-selects: identical coefficient subspace when designs match (sequential Bayes), or PriorMapping.effect_functional(...) when designs differ and the artifact has an effect quantity. Never silent coef_i → coef_i across heterogeneous designs. Ignored when prior_from is a ComposedPrior. backend: Inference backend: laplace (default), conjugate, or hmc.

Bayesian( n_draws: int = 1000, prior_scale: float = 10.0, prior_from: bytes | ComposedPrior | None = None, mapping: PriorMapping | None = None, backend: Literal['laplace', 'conjugate', 'hmc'] = 'laplace', kind: Literal['bayesian'] = 'bayesian')
n_draws: int = 1000
prior_scale: float = 10.0
prior_from: bytes | ComposedPrior | None = None
mapping: PriorMapping | None = None
backend: Literal['laplace', 'conjugate', 'hmc'] = 'laplace'
kind: Literal['bayesian'] = 'bayesian'
@dataclass(frozen=True)
class ConditionalEffect:
70@dataclass(frozen=True)
71class ConditionalEffect:
72    """Conditional / context average effect with a single effect modifier."""
73
74    treatment: str
75    outcome: str
76    modifier: str
77    control_level: float = 0.0
78    active_level: float = 1.0
79    kind: Literal["conditional"] = "conditional"

Conditional / context average effect with a single effect modifier.

ConditionalEffect( treatment: str, outcome: str, modifier: str, control_level: float = 0.0, active_level: float = 1.0, kind: Literal['conditional'] = 'conditional')
treatment: str
outcome: str
modifier: str
control_level: float = 0.0
active_level: float = 1.0
kind: Literal['conditional'] = 'conditional'
@dataclass(frozen=True)
class Counterfactual:
 95@dataclass(frozen=True)
 96class Counterfactual:
 97    """Unit-level ITE via GCM abduction–action–prediction."""
 98
 99    treatment: str
100    outcome: str
101    control_level: float = 0.0
102    active_level: float = 1.0
103    kind: Literal["counterfactual"] = "counterfactual"

Unit-level ITE via GCM abduction–action–prediction.

Counterfactual( treatment: str, outcome: str, control_level: float = 0.0, active_level: float = 1.0, kind: Literal['counterfactual'] = 'counterfactual')
treatment: str
outcome: str
control_level: float = 0.0
active_level: float = 1.0
kind: Literal['counterfactual'] = 'counterfactual'
class Cpdag:

Named static CPDAG.

def from_directed_undirected(cls, /, names, directed, undirected=None):
def from_edges(cls, /, names, edges):
def nodes(self, /):
def edges(self, /):
def parents(self, /, name):
def children(self, /, name):
def undirected_neighbors(self, /, name):
def try_into_dag(self, /):
def node_count(self, /):
def from_dot(cls, /, dot):
def to_dot(self, /):
def from_json(cls, /, json):
def to_json(self, /):
def from_gml(cls, /, gml):
def to_gml(self, /):
class Dag:

Named static DAG.

def from_edges(cls, /, names, edges):
def from_dot(cls, /, dot):
def nodes(self, /):
def edges(self, /):
def parents(self, /, name):
def children(self, /, name):
def node_count(self, /):
def to_dot(self, /):
def from_json(cls, /, json):
def to_json(self, /):
def from_gml(cls, /, gml):
def to_gml(self, /):
def from_networkx_adjacency(cls, /, json):
def to_networkx_adjacency(self, /):
def d_separated(self, /, x, y, z=None):

Whether x is d-separated from y given z.

def latent_project(self, /, observed):

Latent-project onto observed variable names; returns an Admg.

class Estimator(enum.StrEnum):
24class Estimator(StrEnum):
25    """Estimation strategies (wire ids match Rust ``EstimatorId``)."""
26
27    LINEAR_ADJUSTMENT_ATE = "linear.adjustment.ate"
28    PROPENSITY_WEIGHTING = "propensity.weighting"
29    PROPENSITY_MATCHING = "propensity.matching"
30    PROPENSITY_STRATIFICATION = "propensity.stratification"
31    DISTANCE_MATCHING = "distance.matching"
32    AIPW = "aipw"
33    GLM_ADJUSTMENT = "glm.adjustment"
34    FRONTDOOR_TWO_STAGE = "frontdoor.two_stage"
35    IV_WALD = "iv.wald"
36    IV_2SLS = "iv.2sls"
37    RD_SHARP = "rd.sharp"
38    BAYESIAN_GCOMP = "bayesian.gcomp"
39    TEMPORAL_LINEAR_ADJUSTMENT = "temporal.linear.adjustment"
40    FUNCTIONAL_DISTRIBUTION = "functional.distribution"
41    FUNCTIONAL_EFFECT = "functional.effect"
42    CONDITIONAL_LINEAR_ADJUSTMENT = "conditional.linear.adjustment"
43    TEMPORAL_MEDIATION = "temporal.mediation"

Estimation strategies (wire ids match Rust EstimatorId).

LINEAR_ADJUSTMENT_ATE = <Estimator.LINEAR_ADJUSTMENT_ATE: 'linear.adjustment.ate'>
PROPENSITY_WEIGHTING = <Estimator.PROPENSITY_WEIGHTING: 'propensity.weighting'>
PROPENSITY_MATCHING = <Estimator.PROPENSITY_MATCHING: 'propensity.matching'>
PROPENSITY_STRATIFICATION = <Estimator.PROPENSITY_STRATIFICATION: 'propensity.stratification'>
DISTANCE_MATCHING = <Estimator.DISTANCE_MATCHING: 'distance.matching'>
AIPW = <Estimator.AIPW: 'aipw'>
GLM_ADJUSTMENT = <Estimator.GLM_ADJUSTMENT: 'glm.adjustment'>
FRONTDOOR_TWO_STAGE = <Estimator.FRONTDOOR_TWO_STAGE: 'frontdoor.two_stage'>
IV_WALD = <Estimator.IV_WALD: 'iv.wald'>
IV_2SLS = <Estimator.IV_2SLS: 'iv.2sls'>
RD_SHARP = <Estimator.RD_SHARP: 'rd.sharp'>
BAYESIAN_GCOMP = <Estimator.BAYESIAN_GCOMP: 'bayesian.gcomp'>
TEMPORAL_LINEAR_ADJUSTMENT = <Estimator.TEMPORAL_LINEAR_ADJUSTMENT: 'temporal.linear.adjustment'>
FUNCTIONAL_DISTRIBUTION = <Estimator.FUNCTIONAL_DISTRIBUTION: 'functional.distribution'>
FUNCTIONAL_EFFECT = <Estimator.FUNCTIONAL_EFFECT: 'functional.effect'>
CONDITIONAL_LINEAR_ADJUSTMENT = <Estimator.CONDITIONAL_LINEAR_ADJUSTMENT: 'conditional.linear.adjustment'>
TEMPORAL_MEDIATION = <Estimator.TEMPORAL_MEDIATION: 'temporal.mediation'>
@dataclass(frozen=True)
class Frequentist:
13@dataclass(frozen=True)
14class Frequentist:
15    """Frequentist point estimate + bootstrap SE (default)."""
16
17    kind: Literal["frequentist"] = "frequentist"

Frequentist point estimate + bootstrap SE (default).

Frequentist(kind: Literal['frequentist'] = 'frequentist')
kind: Literal['frequentist'] = 'frequentist'
class Identifier(enum.StrEnum):
 9class Identifier(StrEnum):
10    """Identification strategies (wire ids match Rust ``IdentifierId``)."""
11
12    BACKDOOR_ADJUSTMENT = "backdoor.adjustment"
13    BACKDOOR_EFFICIENT = "backdoor.efficient"
14    FRONTDOOR = "frontdoor"
15    IV = "iv"
16    RD_SHARP = "rd.sharp"
17    TEMPORAL_BACKDOOR_UNFOLDED = "temporal.backdoor.unfolded"
18    GENERALIZED_ADJUSTMENT = "generalized.adjustment"
19    GENERAL_ID = "general.id"
20    PATH_SPECIFIC_NATURAL = "path_specific.natural"
21    AUTO = "auto"

Identification strategies (wire ids match Rust IdentifierId).

BACKDOOR_ADJUSTMENT = <Identifier.BACKDOOR_ADJUSTMENT: 'backdoor.adjustment'>
BACKDOOR_EFFICIENT = <Identifier.BACKDOOR_EFFICIENT: 'backdoor.efficient'>
FRONTDOOR = <Identifier.FRONTDOOR: 'frontdoor'>
IV = <Identifier.IV: 'iv'>
RD_SHARP = <Identifier.RD_SHARP: 'rd.sharp'>
TEMPORAL_BACKDOOR_UNFOLDED = <Identifier.TEMPORAL_BACKDOOR_UNFOLDED: 'temporal.backdoor.unfolded'>
GENERALIZED_ADJUSTMENT = <Identifier.GENERALIZED_ADJUSTMENT: 'generalized.adjustment'>
GENERAL_ID = <Identifier.GENERAL_ID: 'general.id'>
PATH_SPECIFIC_NATURAL = <Identifier.PATH_SPECIFIC_NATURAL: 'path_specific.natural'>
AUTO = <Identifier.AUTO: 'auto'>
class Latency(enum.StrEnum):
46class Latency(StrEnum):
47    """Latency tiers (wire ids match Rust ``LatencyMode``)."""
48
49    INTERACTIVE = "interactive"
50    STANDARD = "standard"
51    REPORT = "report"

Latency tiers (wire ids match Rust LatencyMode).

INTERACTIVE = <Latency.INTERACTIVE: 'interactive'>
STANDARD = <Latency.STANDARD: 'standard'>
REPORT = <Latency.REPORT: 'report'>
class Refute(enum.StrEnum):
54class Refute(StrEnum):
55    """Refutation suite ids (also accept ``bool`` at call sites)."""
56
57    FULL = "full"
58    PLACEBO = "placebo"
59    NONE = "none"
60    CHEAP = "cheap"

Refutation suite ids (also accept bool at call sites).

FULL = <Refute.FULL: 'full'>
PLACEBO = <Refute.PLACEBO: 'placebo'>
NONE = <Refute.NONE: 'none'>
CHEAP = <Refute.CHEAP: 'cheap'>
@dataclass(frozen=True)
class IdentifyResult:
935@dataclass(frozen=True)
936class IdentifyResult:
937    """Identify-only result (no estimate)."""
938
939    status: str
940    method: str
941    adjustment_set: list[str]

Identify-only result (no estimate).

IdentifyResult(status: str, method: str, adjustment_set: list[str])
status: str
method: str
adjustment_set: list[str]
@dataclass(frozen=True)
class InterventionalDistribution:
46@dataclass(frozen=True)
47class InterventionalDistribution:
48    """Interventional distribution query (static)."""
49
50    outcome: str
51    interventions: dict[str, float] = field(default_factory=dict)
52    conditioning: Sequence[str] = ()
53    kind: Literal["distribution"] = "distribution"

Interventional distribution query (static).

InterventionalDistribution( outcome: str, interventions: dict[str, float] = <factory>, conditioning: Sequence[str] = (), kind: Literal['distribution'] = 'distribution')
outcome: str
interventions: dict[str, float]
conditioning: Sequence[str] = ()
kind: Literal['distribution'] = 'distribution'
@dataclass(frozen=True)
class MediationEffect:
82@dataclass(frozen=True)
83class MediationEffect:
84    """Static mediation (treatment → mediator(s) → outcome)."""
85
86    treatment: str
87    outcome: str
88    mediators: Sequence[str]
89    contrast: Literal["total", "direct", "mediated"] = "mediated"
90    control_level: float = 0.0
91    active_level: float = 1.0
92    kind: Literal["mediation"] = "mediation"

Static mediation (treatment → mediator(s) → outcome).

MediationEffect( treatment: str, outcome: str, mediators: Sequence[str], contrast: Literal['total', 'direct', 'mediated'] = 'mediated', control_level: float = 0.0, active_level: float = 1.0, kind: Literal['mediation'] = 'mediation')
treatment: str
outcome: str
mediators: Sequence[str]
contrast: Literal['total', 'direct', 'mediated'] = 'mediated'
control_level: float = 0.0
active_level: float = 1.0
kind: Literal['mediation'] = 'mediation'
class Pag:

Named static PAG.

def from_marked_edges(cls, /, names, edges):
def nodes(self, /):
def neighbors(self, /, name):
def directed_children(self, /, name):
def node_count(self, /):
def from_dot(cls, /, dot):
def to_dot(self, /):
def from_json(cls, /, json):
def to_json(self, /):
def from_gml(cls, /, gml):
def to_gml(self, /):
def m_separated(self, /, x, y, z=None, *, max_paths=32, max_len=6):

Definite-status m-separation of x and y given z.

@dataclass(frozen=True)
class PathSpecificEffect:
56@dataclass(frozen=True)
57class PathSpecificEffect:
58    """Path-specific effect query (static)."""
59
60    treatment: str
61    outcome: str
62    path_nodes: Sequence[str] | None = None
63    control_level: float = 0.0
64    active_level: float = 1.0
65    max_paths: int = 64
66    max_len: int = 16
67    kind: Literal["path_specific"] = "path_specific"

Path-specific effect query (static).

PathSpecificEffect( treatment: str, outcome: str, path_nodes: Optional[Sequence[str]] = None, control_level: float = 0.0, active_level: float = 1.0, max_paths: int = 64, max_len: int = 16, kind: Literal['path_specific'] = 'path_specific')
treatment: str
outcome: str
path_nodes: Optional[Sequence[str]] = None
control_level: float = 0.0
active_level: float = 1.0
max_paths: int = 64
max_len: int = 16
kind: Literal['path_specific'] = 'path_specific'
class PreparedAnalysis:
1754class PreparedAnalysis:
1755    """Compile-once / re-estimate-many handle for static AverageEffect on a DAG.
1756
1757    Use for interactive sessions: prepare with a fixed graph/query/estimator,
1758    then call :meth:`estimate` or :meth:`refresh` when the table changes
1759    (same schema). Prefer this over fresh :func:`analyze` on every click.
1760    For streaming append + incremental OLS, use :class:`antecedent.CausalState`.
1761    """
1762
1763    def __init__(self, native: Any) -> None:
1764        self._native = native
1765
1766    @classmethod
1767    def prepare(
1768        cls,
1769        data: Mapping[str, Any] | Any,
1770        *,
1771        query: AverageEffect,
1772        graph: Dag | Sequence[tuple[str, str]],
1773        inference: Frequentist | Bayesian | None = None,
1774        identifier: str | Identifier | None = None,
1775        estimator: str | Estimator | None = None,
1776        refute: bool | Refute | Literal["full", "placebo", "none", "cheap"] = False,
1777        seed: int = 1,
1778        bootstrap: int | None = None,
1779        threads: int = 1,
1780        latency: Latency | Literal["interactive", "standard", "report"] | None = "interactive",
1781    ) -> PreparedAnalysis:
1782        """Compile a durable plan for static ATE on a supplied DAG."""
1783        if not isinstance(query, AverageEffect):
1784            raise TypeError("PreparedAnalysis supports AverageEffect only")
1785        inference = inference or Frequentist()
1786        if isinstance(identifier, Identifier):
1787            identifier = str(identifier)
1788        if isinstance(estimator, Estimator):
1789            estimator = str(estimator)
1790        if isinstance(latency, Latency):
1791            latency = str(latency)  # type: ignore[assignment]
1792        if isinstance(refute, Refute):
1793            refute = str(refute)  # type: ignore[assignment]
1794        bootstrap, refute = _resolve_latency_budget(latency, bootstrap, refute)
1795        names, columns = as_columns(data)  # type: ignore[arg-type]
1796        edges = _static_edges(graph)  # type: ignore[arg-type]
1797        bayes_kw: dict[str, Any] = {}
1798        if isinstance(inference, Bayesian):
1799            bayes_kw = _bayesian_inference_kwargs(inference)
1800            inference_mode = str(bayes_kw.pop("inference"))
1801        else:
1802            inference_mode = "frequentist"
1803        native = _NativePreparedAnalysis.prepare(
1804            names,
1805            columns,
1806            edges,
1807            query.treatment,
1808            query.outcome,
1809            control_level=query.control_level,
1810            active_level=query.active_level,
1811            identifier=identifier,
1812            estimator=estimator,
1813            inference=inference_mode,
1814            n_draws=int(bayes_kw.get("n_draws", 1000)),
1815            prior_scale=float(bayes_kw.get("prior_scale", 10.0)),
1816            refute=refute,
1817            seed=seed,
1818            bootstrap=bootstrap,
1819            threads=threads,
1820            latency=latency,
1821        )
1822        return cls(native)
1823
1824    @property
1825    def plan(self) -> PhysicalPlanView:
1826        """Physical-plan summary retained from prepare."""
1827        raw = self._native.plan_summary()
1828        return PhysicalPlanView(
1829            plan_id=str(raw.get("plan_id", "")),
1830            estimated_peak_memory_bytes=(
1831                int(raw["estimated_peak_memory_bytes"])
1832                if "estimated_peak_memory_bytes" in raw
1833                else None
1834            ),
1835            workspace_bytes=(
1836                int(raw["workspace_bytes"]) if "workspace_bytes" in raw else None
1837            ),
1838            batch_size=int(raw["batch_size"]) if "batch_size" in raw else None,
1839            worker_threads=int(raw.get("worker_threads", 0)),
1840            expected_python_crossings=int(raw.get("expected_python_crossings", 0)),
1841            deterministic_reductions=str(raw.get("deterministic_reductions", "true")).lower()
1842            in ("1", "true"),
1843            kernels=raw.get("kernels") or None,
1844        )
1845
1846    def estimate(
1847        self,
1848        data: Mapping[str, Any] | Any,
1849        *,
1850        seed: int = 1,
1851        threads: int = 1,
1852    ) -> AnalysisResult:
1853        """Re-estimate without recompiling (same schema as prepare)."""
1854        names, columns = as_columns(data)  # type: ignore[arg-type]
1855        raw = self._native.estimate(names, columns, seed=seed, threads=threads)
1856        return _wrap_ate(raw, prepared=self)
1857
1858    def refresh(
1859        self,
1860        data: Mapping[str, Any] | Any,
1861        *,
1862        seed: int = 1,
1863        threads: int = 1,
1864    ) -> AnalysisResult:
1865        """Replace retained data and re-estimate."""
1866        names, columns = as_columns(data)  # type: ignore[arg-type]
1867        raw = self._native.refresh(names, columns, seed=seed, threads=threads)
1868        return _wrap_ate(raw, prepared=self)
1869
1870    def refute(
1871        self,
1872        data: Mapping[str, Any] | Any,
1873        suite: Refute | Literal["placebo", "full", "cheap"] | bool | str = "placebo",
1874        *,
1875        seed: int = 1,
1876        threads: int = 1,
1877        cancel: Any | None = None,
1878    ) -> AnalysisResult:
1879        """Second-click refute against the last :meth:`estimate` / :meth:`refresh`.
1880
1881        Interactive first clicks typically use ``refute=False`` or ``cheap``;
1882        call this with ``suite="placebo"`` or ``"full"`` for the deferred suite.
1883        """
1884        if isinstance(suite, Refute):
1885            suite = str(suite)
1886        names, columns = as_columns(data)  # type: ignore[arg-type]
1887        kwargs: dict[str, Any] = dict(seed=seed, threads=threads)
1888        if cancel is not None:
1889            kwargs["cancel"] = cancel
1890        raw = self._native.refute(names, columns, suite, **kwargs)
1891        return _wrap_ate(raw, prepared=self)

Compile-once / re-estimate-many handle for static AverageEffect on a DAG.

Use for interactive sessions: prepare with a fixed graph/query/estimator, then call estimate() or refresh() when the table changes (same schema). Prefer this over fresh analyze() on every click. For streaming append + incremental OLS, use antecedent.CausalState.

PreparedAnalysis(native: Any)
1763    def __init__(self, native: Any) -> None:
1764        self._native = native
@classmethod
def prepare( cls, data: Union[Mapping[str, Any], Any], *, query: AverageEffect, graph: Union[Dag, Sequence[tuple[str, str]]], inference: Frequentist | Bayesian | None = None, identifier: str | Identifier | None = None, estimator: str | Estimator | None = None, refute: Union[bool, Refute, Literal['full', 'placebo', 'none', 'cheap']] = False, seed: int = 1, bootstrap: int | None = None, threads: int = 1, latency: Union[Latency, Literal['interactive', 'standard', 'report'], NoneType] = 'interactive') -> PreparedAnalysis:
1766    @classmethod
1767    def prepare(
1768        cls,
1769        data: Mapping[str, Any] | Any,
1770        *,
1771        query: AverageEffect,
1772        graph: Dag | Sequence[tuple[str, str]],
1773        inference: Frequentist | Bayesian | None = None,
1774        identifier: str | Identifier | None = None,
1775        estimator: str | Estimator | None = None,
1776        refute: bool | Refute | Literal["full", "placebo", "none", "cheap"] = False,
1777        seed: int = 1,
1778        bootstrap: int | None = None,
1779        threads: int = 1,
1780        latency: Latency | Literal["interactive", "standard", "report"] | None = "interactive",
1781    ) -> PreparedAnalysis:
1782        """Compile a durable plan for static ATE on a supplied DAG."""
1783        if not isinstance(query, AverageEffect):
1784            raise TypeError("PreparedAnalysis supports AverageEffect only")
1785        inference = inference or Frequentist()
1786        if isinstance(identifier, Identifier):
1787            identifier = str(identifier)
1788        if isinstance(estimator, Estimator):
1789            estimator = str(estimator)
1790        if isinstance(latency, Latency):
1791            latency = str(latency)  # type: ignore[assignment]
1792        if isinstance(refute, Refute):
1793            refute = str(refute)  # type: ignore[assignment]
1794        bootstrap, refute = _resolve_latency_budget(latency, bootstrap, refute)
1795        names, columns = as_columns(data)  # type: ignore[arg-type]
1796        edges = _static_edges(graph)  # type: ignore[arg-type]
1797        bayes_kw: dict[str, Any] = {}
1798        if isinstance(inference, Bayesian):
1799            bayes_kw = _bayesian_inference_kwargs(inference)
1800            inference_mode = str(bayes_kw.pop("inference"))
1801        else:
1802            inference_mode = "frequentist"
1803        native = _NativePreparedAnalysis.prepare(
1804            names,
1805            columns,
1806            edges,
1807            query.treatment,
1808            query.outcome,
1809            control_level=query.control_level,
1810            active_level=query.active_level,
1811            identifier=identifier,
1812            estimator=estimator,
1813            inference=inference_mode,
1814            n_draws=int(bayes_kw.get("n_draws", 1000)),
1815            prior_scale=float(bayes_kw.get("prior_scale", 10.0)),
1816            refute=refute,
1817            seed=seed,
1818            bootstrap=bootstrap,
1819            threads=threads,
1820            latency=latency,
1821        )
1822        return cls(native)

Compile a durable plan for static ATE on a supplied DAG.

1824    @property
1825    def plan(self) -> PhysicalPlanView:
1826        """Physical-plan summary retained from prepare."""
1827        raw = self._native.plan_summary()
1828        return PhysicalPlanView(
1829            plan_id=str(raw.get("plan_id", "")),
1830            estimated_peak_memory_bytes=(
1831                int(raw["estimated_peak_memory_bytes"])
1832                if "estimated_peak_memory_bytes" in raw
1833                else None
1834            ),
1835            workspace_bytes=(
1836                int(raw["workspace_bytes"]) if "workspace_bytes" in raw else None
1837            ),
1838            batch_size=int(raw["batch_size"]) if "batch_size" in raw else None,
1839            worker_threads=int(raw.get("worker_threads", 0)),
1840            expected_python_crossings=int(raw.get("expected_python_crossings", 0)),
1841            deterministic_reductions=str(raw.get("deterministic_reductions", "true")).lower()
1842            in ("1", "true"),
1843            kernels=raw.get("kernels") or None,
1844        )

Physical-plan summary retained from prepare.

def estimate( self, data: Union[Mapping[str, Any], Any], *, seed: int = 1, threads: int = 1) -> AnalysisResult:
1846    def estimate(
1847        self,
1848        data: Mapping[str, Any] | Any,
1849        *,
1850        seed: int = 1,
1851        threads: int = 1,
1852    ) -> AnalysisResult:
1853        """Re-estimate without recompiling (same schema as prepare)."""
1854        names, columns = as_columns(data)  # type: ignore[arg-type]
1855        raw = self._native.estimate(names, columns, seed=seed, threads=threads)
1856        return _wrap_ate(raw, prepared=self)

Re-estimate without recompiling (same schema as prepare).

def refresh( self, data: Union[Mapping[str, Any], Any], *, seed: int = 1, threads: int = 1) -> AnalysisResult:
1858    def refresh(
1859        self,
1860        data: Mapping[str, Any] | Any,
1861        *,
1862        seed: int = 1,
1863        threads: int = 1,
1864    ) -> AnalysisResult:
1865        """Replace retained data and re-estimate."""
1866        names, columns = as_columns(data)  # type: ignore[arg-type]
1867        raw = self._native.refresh(names, columns, seed=seed, threads=threads)
1868        return _wrap_ate(raw, prepared=self)

Replace retained data and re-estimate.

def refute( self, data: Union[Mapping[str, Any], Any], suite: Union[Refute, Literal['placebo', 'full', 'cheap'], bool, str] = 'placebo', *, seed: int = 1, threads: int = 1, cancel: typing.Any | None = None) -> AnalysisResult:
1870    def refute(
1871        self,
1872        data: Mapping[str, Any] | Any,
1873        suite: Refute | Literal["placebo", "full", "cheap"] | bool | str = "placebo",
1874        *,
1875        seed: int = 1,
1876        threads: int = 1,
1877        cancel: Any | None = None,
1878    ) -> AnalysisResult:
1879        """Second-click refute against the last :meth:`estimate` / :meth:`refresh`.
1880
1881        Interactive first clicks typically use ``refute=False`` or ``cheap``;
1882        call this with ``suite="placebo"`` or ``"full"`` for the deferred suite.
1883        """
1884        if isinstance(suite, Refute):
1885            suite = str(suite)
1886        names, columns = as_columns(data)  # type: ignore[arg-type]
1887        kwargs: dict[str, Any] = dict(seed=seed, threads=threads)
1888        if cancel is not None:
1889            kwargs["cancel"] = cancel
1890        raw = self._native.refute(names, columns, suite, **kwargs)
1891        return _wrap_ate(raw, prepared=self)

Second-click refute against the last estimate() / refresh().

Interactive first clicks typically use refute=False or cheap; call this with suite="placebo" or "full" for the deferred suite.

@dataclass(frozen=True)
class PulseEffect:
22@dataclass(frozen=True)
23class PulseEffect:
24    """Temporal pulse intervention effect."""
25
26    treatment: str
27    outcome: str
28    active_level: float = 1.0
29    treatment_lag: int = 1
30    horizon_steps: int = 1
31    kind: Literal["pulse"] = "pulse"

Temporal pulse intervention effect.

PulseEffect( treatment: str, outcome: str, active_level: float = 1.0, treatment_lag: int = 1, horizon_steps: int = 1, kind: Literal['pulse'] = 'pulse')
treatment: str
outcome: str
active_level: float = 1.0
treatment_lag: int = 1
horizon_steps: int = 1
kind: Literal['pulse'] = 'pulse'
@dataclass(frozen=True)
class SustainedEffect:
34@dataclass(frozen=True)
35class SustainedEffect:
36    """Temporal sustained intervention effect."""
37
38    treatment: str
39    outcome: str
40    active_level: float = 1.0
41    treatment_lag: int = 1
42    horizon_steps: int = 1
43    kind: Literal["sustained"] = "sustained"

Temporal sustained intervention effect.

SustainedEffect( treatment: str, outcome: str, active_level: float = 1.0, treatment_lag: int = 1, horizon_steps: int = 1, kind: Literal['sustained'] = 'sustained')
treatment: str
outcome: str
active_level: float = 1.0
treatment_lag: int = 1
horizon_steps: int = 1
kind: Literal['sustained'] = 'sustained'
class TemporalDag:

Named temporal DAG over lagged variables.

def from_lagged_edges(cls, /, names, edges):
def nodes(self, /):

Lagged nodes as (name, lag) pairs.

def edges(self, /):

Directed edges as (src, src_lag, tgt, tgt_lag).

def node_count(self, /):
@dataclass(frozen=True)
class TemporalMediationEffect:
106@dataclass(frozen=True)
107class TemporalMediationEffect:
108    """Temporal linear mediation (treatment → mediator → outcome)."""
109
110    treatment: str
111    mediator: str
112    outcome: str
113    contrast: Literal["total", "direct", "mediated"] = "mediated"
114    control_level: float = 0.0
115    active_level: float = 1.0
116    kind: Literal["temporal_mediation"] = "temporal_mediation"

Temporal linear mediation (treatment → mediator → outcome).

TemporalMediationEffect( treatment: str, mediator: str, outcome: str, contrast: Literal['total', 'direct', 'mediated'] = 'mediated', control_level: float = 0.0, active_level: float = 1.0, kind: Literal['temporal_mediation'] = 'temporal_mediation')
treatment: str
mediator: str
outcome: str
contrast: Literal['total', 'direct', 'mediated'] = 'mediated'
control_level: float = 0.0
active_level: float = 1.0
kind: Literal['temporal_mediation'] = 'temporal_mediation'
def analyze( data: Union[Mapping[str, Any], Any, Sequence[Union[Mapping[str, Any], Any]]], *, query: AverageEffect | PulseEffect | SustainedEffect | InterventionalDistribution | PathSpecificEffect | ConditionalEffect | MediationEffect | Counterfactual | TemporalMediationEffect, graph: Union[Dag, Cpdag, Pag, Admg, TemporalDag, TemporalCpdag, TemporalPag, Sequence[tuple[str, str]], Sequence[tuple[str, int, str, int]], NoneType] = None, discovery: typing.Any | None = None, inference: Frequentist | Bayesian | None = None, identifier: str | Identifier | None = None, estimator: str | Estimator | None = None, refute: Union[bool, Refute, Literal['full', 'placebo', 'none', 'cheap']] = True, validators: Optional[Sequence[Any]] = None, accept_discovered: bool = True, seed: int = 1, bootstrap: int | None = None, threads: int = 1, regimes: Optional[Sequence[int]] = None, running_variable: str | None = None, cutoff: float | None = None, bandwidth: float | None = None, population_registry: typing.Any | None = None, latency: Union[Latency, Literal['interactive', 'standard', 'report'], NoneType] = None, cancel: typing.Any | None = None, on_progress: typing.Any | None = None, on_stage: typing.Any | None = None, return_posterior_artifact: bool = False) -> AnalysisResult:
 976def analyze(
 977    data: Mapping[str, Any] | Any | Sequence[Mapping[str, Any] | Any],
 978    *,
 979    query: (
 980        AverageEffect
 981        | PulseEffect
 982        | SustainedEffect
 983        | InterventionalDistribution
 984        | PathSpecificEffect
 985        | ConditionalEffect
 986        | MediationEffect
 987        | Counterfactual
 988        | TemporalMediationEffect
 989    ),
 990    graph: (
 991        Dag
 992        | Cpdag
 993        | Pag
 994        | Admg
 995        | TemporalDag
 996        | TemporalCpdag
 997        | TemporalPag
 998        | Sequence[tuple[str, str]]
 999        | Sequence[tuple[str, int, str, int]]
1000        | None
1001    ) = None,
1002    discovery: Any | None = None,
1003    inference: Frequentist | Bayesian | None = None,
1004    identifier: str | Identifier | None = None,
1005    estimator: str | Estimator | None = None,
1006    refute: bool | Refute | Literal["full", "placebo", "none", "cheap"] = True,
1007    validators: Sequence[Any] | None = None,
1008    accept_discovered: bool = True,
1009    seed: int = 1,
1010    bootstrap: int | None = None,
1011    threads: int = 1,
1012    regimes: Sequence[int] | None = None,
1013    running_variable: str | None = None,
1014    cutoff: float | None = None,
1015    bandwidth: float | None = None,
1016    population_registry: Any | None = None,
1017    latency: Latency | Literal["interactive", "standard", "report"] | None = None,
1018    cancel: Any | None = None,
1019    on_progress: Any | None = None,
1020    on_stage: Any | None = None,
1021    return_posterior_artifact: bool = False,
1022) -> AnalysisResult:
1023    """Identify then estimate a causal effect.
1024
1025    Parameters
1026    ----------
1027    data:
1028        Mapping of column name → 1-d float array, a pandas ``DataFrame``,
1029        Arrow CDI exporters (PyArrow columns / table), or a
1030        ``antecedent.data`` frame (``EventFrame`` / ``PanelFrame`` / ``MultiEnvFrame``).
1031        For ``discovery=JPCMCIPlus(...)``, pass a sequence of environment frames
1032        or a ``MultiEnvFrame``.
1033    query:
1034        ``AverageEffect``, ``PulseEffect`` / ``SustainedEffect``,
1035        ``InterventionalDistribution``, ``PathSpecificEffect``,
1036        ``MediationEffect``, ``Counterfactual``, or ``TemporalMediationEffect``.
1037    graph:
1038        ``Dag`` / ``Cpdag`` / ``Pag`` / ``Admg`` / ``TemporalDag`` /
1039        ``TemporalCpdag`` / ``TemporalPag``, or an edge list. Lagged edges
1040        ``(from, from_lag, to, to_lag)`` are required for temporal queries
1041        without ``discovery``. Fully oriented CPDAGs run as DAGs; incomplete
1042        CPDAGs require review. ADMGs without bidirected edges coerce to DAGs;
1043        ADMGs with latents use general ID + functional effect.
1044    discovery:
1045        Static: ``PC`` / ``GES`` / ``LiNGAM`` / ``NOTEARS`` / ``FCI`` / ``RFCI``.
1046        Temporal: ``PCMCI`` / ``PCMCIPlus`` / ``LPCMCI`` / ``JPCMCIPlus`` / ``RPCMCI``.
1047        One-shot script convenience — discovery runs at compile time. For
1048        interactive / spreadsheet estimate clicks, discover once into
1049        :class:`antecedent.AcceptedGraph` (or hold a reviewed graph) and pass
1050        ``graph=`` with ``latency="interactive"`` instead. Combining
1051        ``discovery=`` with ``latency="interactive"`` raises
1052        :class:`CausalUnsupportedError`.
1053    latency:
1054        Optional compute tier (``interactive`` / ``standard`` / ``report``).
1055        Maps to known-equivalent bootstrap / refute / draws; explicit
1056        ``bootstrap=`` / ``refute=`` always win. Interactive refuses inline
1057        ``discovery=`` (artifact-first UX).
1058    cancel:
1059        Optional ``CancellationToken`` from ``antecedent._native``.
1060    on_progress:
1061        Optional ``(fraction: float, stage: str) -> None`` callback.
1062    on_stage:
1063        Optional ``(stage: str, payload: dict) -> None`` progressive stage
1064        callback (identify → estimate_point → uncertainty → validate).
1065    return_posterior_artifact:
1066        When ``True`` and inference is Bayesian, attach full posterior draw
1067        bytes on ``result.posterior.artifact`` (for download / sequential-prior
1068        hydrate). Default ``False``: UI summaries only.
1069    """
1070    if isinstance(identifier, Identifier):
1071        identifier = str(identifier)
1072    if isinstance(estimator, Estimator):
1073        estimator = str(estimator)
1074    if isinstance(latency, Latency):
1075        latency = str(latency)  # type: ignore[assignment]
1076    if isinstance(refute, Refute):
1077        refute = str(refute)  # type: ignore[assignment]
1078    inference = inference or Frequentist()
1079    bootstrap, refute = _resolve_latency_budget(latency, bootstrap, refute)
1080
1081    if discovery is not None and latency == "interactive":
1082        raise CausalUnsupportedError(
1083            "discovery= is not on the interactive estimate path; "
1084            "call discover_* once, accept into AcceptedGraph, then "
1085            "analyze(graph=..., latency='interactive')"
1086        )
1087
1088    if isinstance(query, ConditionalEffect):
1089        if isinstance(inference, Bayesian):
1090            raise TypeError("ConditionalEffect does not support inference=Bayesian(...)")
1091        if discovery is not None:
1092            raise ValueError("ConditionalEffect does not support discovery=")
1093        names, columns = as_columns(data)  # type: ignore[arg-type]
1094        edges = _static_edges(graph)  # type: ignore[arg-type]
1095        raw = _analyze_conditional(
1096            names,
1097            columns,
1098            edges,
1099            query.treatment,
1100            query.outcome,
1101            query.modifier,
1102            control_level=query.control_level,
1103            active_level=query.active_level,
1104            refute=refute,
1105            validators=list(validators) if validators is not None else None,
1106            seed=seed,
1107            bootstrap=bootstrap,
1108            threads=threads,
1109        )
1110        return _wrap_ate(raw)
1111
1112    if isinstance(query, TemporalMediationEffect):
1113        if isinstance(inference, Bayesian):
1114            raise TypeError("TemporalMediationEffect does not support inference=Bayesian(...)")
1115        if discovery is not None:
1116            raise ValueError("TemporalMediationEffect does not support discovery=")
1117        names, columns = as_columns(data)  # type: ignore[arg-type]
1118        lagged = _lagged_edges(graph)  # type: ignore[arg-type]
1119        raw = _analyze_temporal_mediation(
1120            names,
1121            columns,
1122            lagged,
1123            query.treatment,
1124            query.mediator,
1125            query.outcome,
1126            contrast=query.contrast,
1127            control_level=query.control_level,
1128            active_level=query.active_level,
1129            seed=seed,
1130            bootstrap=bootstrap,
1131            threads=threads,
1132        )
1133        return _wrap_temporal(raw)
1134
1135    if isinstance(query, MediationEffect):
1136        if discovery is not None:
1137            raise ValueError("MediationEffect does not support discovery=")
1138        edges = _static_edges(graph)  # type: ignore[arg-type]
1139        names, columns = as_columns(data)  # type: ignore[arg-type]
1140        raw = _analyze_mediation(
1141            names,
1142            columns,
1143            edges,
1144            query.treatment,
1145            query.outcome,
1146            list(query.mediators),
1147            contrast=query.contrast,
1148            control_level=query.control_level,
1149            active_level=query.active_level,
1150            refute=refute,
1151            seed=seed,
1152            bootstrap=bootstrap,
1153            threads=threads,
1154        )
1155        return _wrap_ate(raw)
1156
1157    if isinstance(query, Counterfactual):
1158        from ._native import counterfactual_ite
1159
1160        if discovery is not None:
1161            raise ValueError("Counterfactual does not support discovery=")
1162        edges = _static_edges(graph)  # type: ignore[arg-type]
1163        names, columns = as_columns(data)  # type: ignore[arg-type]
1164        ite = counterfactual_ite(
1165            names,
1166            columns,
1167            edges,
1168            query.treatment,
1169            query.outcome,
1170            query.active_level,
1171            query.control_level,
1172            seed=seed,
1173            threads=threads,
1174        )
1175        return AnalysisResult(
1176            identification=IdentificationView(
1177                status="gcm.parametric",
1178                method="counterfactual.ite",
1179                adjustment_set=[],
1180                assumption_count=0,
1181                derivation_step_count=0,
1182            ),
1183            estimate=EstimateView(
1184                ate=float(ite.mean_ite),
1185                se_analytic=float("nan"),
1186                se_bootstrap=None,
1187                estimator_id="gcm.ite",
1188                method="counterfactual.ite",
1189            ),
1190            posterior=None,
1191            validation=ValidationView(passed=False, ran=False, count=0),
1192            performance=PerformanceView(
1193                plan_id="counterfactual.ite",
1194                modality="static",
1195                peak_memory_bytes=0,
1196            ),
1197            diagnostics=[],
1198            provenance={"noise_inference": getattr(ite, "noise_inference", None)},
1199            _raw=ite,
1200        )
1201
1202    if isinstance(query, InterventionalDistribution):
1203        if discovery is not None:
1204            edges = _resolve_static_discovery_edges(
1205                data, discovery, accept_discovered, seed, threads
1206            )
1207        else:
1208            edges = _static_edges(graph)  # type: ignore[arg-type]
1209        names, columns = as_columns(data)  # type: ignore[arg-type]
1210        raw = _analyze_distribution(
1211            names,
1212            columns,
1213            edges,
1214            query.outcome,
1215            dict(query.interventions),
1216            conditioning=list(query.conditioning) or None,
1217            seed=seed,
1218            threads=threads,
1219        )
1220        return _wrap_ate(raw)
1221
1222    if isinstance(query, PathSpecificEffect):
1223        if discovery is not None:
1224            edges = _resolve_static_discovery_edges(
1225                data, discovery, accept_discovered, seed, threads
1226            )
1227        else:
1228            edges = _static_edges(graph)  # type: ignore[arg-type]
1229        names, columns = as_columns(data)  # type: ignore[arg-type]
1230        raw = _analyze_path_specific(
1231            names,
1232            columns,
1233            edges,
1234            query.treatment,
1235            query.outcome,
1236            control_level=query.control_level,
1237            active_level=query.active_level,
1238            path_nodes=list(query.path_nodes) if query.path_nodes is not None else None,
1239            max_paths=query.max_paths,
1240            max_len=query.max_len,
1241            seed=seed,
1242            bootstrap=bootstrap,
1243            threads=threads,
1244        )
1245        return _wrap_ate(raw)
1246
1247    if discovery is not None and isinstance(
1248        discovery, _STATIC_DISCOVERY + _GRAPH_POSTERIOR_DISCOVERY
1249    ):
1250        if not isinstance(query, AverageEffect):
1251            raise ValueError(
1252                f"discovery={type(discovery).__name__}(...) requires AverageEffect"
1253            )
1254        if isinstance(discovery, _GRAPH_POSTERIOR_DISCOVERY) and not isinstance(
1255            inference, Bayesian
1256        ):
1257            raise TypeError(
1258                "graph-posterior discovery requires inference=Bayesian(...) "
1259                "for effect mixture"
1260            )
1261        names, columns = as_columns(data)  # type: ignore[arg-type]
1262        cfg = _discovery_algorithm(discovery)
1263        bayes_kw: dict[str, Any] = {}
1264        if isinstance(inference, Bayesian):
1265            bayes_kw = _bayesian_inference_kwargs(inference)
1266        raw = _analyze_ate_discover(
1267            names,
1268            columns,
1269            query.treatment,
1270            query.outcome,
1271            algorithm=cfg["algorithm"],
1272            alpha=cfg.get("alpha", 0.05),
1273            fdr=cfg.get("fdr", True),
1274            max_cond_size=cfg.get("max_cond_size", 2),
1275            prune_threshold=cfg.get("prune_threshold", 0.0),
1276            l1=cfg.get("lambda", 0.1),
1277            threshold=cfg.get("threshold", 0.3),
1278            standardize=cfg.get("standardize", True),
1279            accept_discovered=accept_discovered,
1280            control_level=query.control_level,
1281            active_level=query.active_level,
1282            identifier=identifier,
1283            estimator=estimator,
1284            refute=refute,
1285            validators=list(validators) if validators is not None else None,
1286            ci=cfg.get("ci"),
1287            n_chains=cfg.get("n_chains", 2),
1288            n_warmup=cfg.get("n_warmup", 100),
1289            mcmc_draws=cfg.get("mcmc_draws", 200),
1290            thin=cfg.get("thin", 1),
1291            soft_weight=cfg.get("soft_weight", "none"),
1292            require_diagnostics_gate=cfg.get("require_diagnostics_gate", True),
1293            seed=seed,
1294            bootstrap=bootstrap,
1295            threads=threads,
1296            **bayes_kw,
1297        )
1298        return _wrap_ate(raw)
1299
1300    if discovery is not None and isinstance(query, AverageEffect):
1301        raise ValueError(
1302            "AverageEffect with discovery= requires a static algorithm "
1303            "(PC/GES/LiNGAM/NOTEARS/FCI/RFCI); temporal discovery needs "
1304            "PulseEffect/SustainedEffect"
1305        )
1306
1307    if isinstance(query, AverageEffect):
1308        bayes_kw: dict[str, Any] = {}
1309        if isinstance(inference, Bayesian):
1310            bayes_kw = _bayesian_inference_kwargs(inference)
1311        if estimator == "rd.sharp" or any(
1312            v is not None for v in (running_variable, cutoff, bandwidth)
1313        ):
1314            if running_variable is None or cutoff is None or bandwidth is None:
1315                raise ValueError(
1316                    "rd.sharp (or any RD kwargs) requires running_variable, cutoff, and bandwidth"
1317                )
1318            if estimator is None:
1319                estimator = "rd.sharp"
1320            if identifier is None:
1321                identifier = "rd.sharp"
1322        common = dict(
1323            treatment=query.treatment,
1324            outcome=query.outcome,
1325            control_level=query.control_level,
1326            active_level=query.active_level,
1327            identifier=identifier,
1328            estimator=estimator,
1329            refute=refute,
1330            validators=list(validators) if validators is not None else None,
1331            running_variable=running_variable,
1332            cutoff=cutoff,
1333            bandwidth=bandwidth,
1334            seed=seed,
1335            bootstrap=bootstrap,
1336            threads=threads,
1337            **bayes_kw,
1338        )
1339        if return_posterior_artifact:
1340            common["return_posterior_artifact"] = True
1341        if latency is not None:
1342            common["latency"] = latency
1343        if cancel is not None:
1344            common["cancel"] = cancel
1345        if on_progress is not None:
1346            common["on_progress"] = on_progress
1347        if on_stage is not None:
1348            common["on_stage"] = on_stage
1349        from .population import coerce_target_population, registry_wire
1350
1351        pop = coerce_target_population(
1352            getattr(query, "target_population", None)
1353        )
1354        preds, dists = registry_wire(population_registry)
1355        pop_kw: dict[str, Any] = {}
1356        if pop is not None:
1357            pop_kw["target_population"] = pop
1358        if preds:
1359            pop_kw["population_predicates"] = preds
1360        if dists:
1361            pop_kw["population_distributions"] = dists
1362        if pop_kw and isinstance(graph, (Pag, Cpdag, Admg)):
1363            raise ValueError(
1364                "target_population / population_registry currently require a Dag "
1365                "(or edge list); PAG/CPDAG/ADMG analyze paths do not accept them yet"
1366            )
1367        if isinstance(graph, Pag):
1368            names, columns = as_columns(data)  # type: ignore[arg-type]
1369            return _wrap_ate(_analyze_ate_pag(names, columns, graph, **common))
1370        if isinstance(graph, Cpdag):
1371            names, columns = as_columns(data)  # type: ignore[arg-type]
1372            return _wrap_ate(_analyze_ate_cpdag(names, columns, graph, **common))
1373        if isinstance(graph, Admg):
1374            names, columns = as_columns(data)  # type: ignore[arg-type]
1375            return _wrap_ate(_analyze_ate_admg(names, columns, graph, **common))
1376        edges = _static_edges(graph)  # type: ignore[arg-type]
1377        arrow = try_as_arrow_c_columns(data)
1378        ate_kwargs = dict(edges=edges, **common, **pop_kw)
1379        # Prefer Arrow CDI (zero-copy float64) when available. Population kwargs
1380        # still require the NumPy path until that surface is wired on CDI.
1381        use_arrow = arrow is not None and not pop_kw
1382        if use_arrow:
1383            names, columns = arrow
1384            raw = _analyze_ate_arrow_c(names, columns, **ate_kwargs)
1385        else:
1386            names, columns = as_columns(data)  # type: ignore[arg-type]
1387            raw = _analyze_ate(names, columns, **ate_kwargs)
1388        return _wrap_ate(raw)
1389
1390    if isinstance(query, (PulseEffect, SustainedEffect)):
1391        policy = "sustained" if isinstance(query, SustainedEffect) else "pulse"
1392        _reject_unsupported_temporal(
1393            inference=inference, refute=refute, validators=validators
1394        )
1395        bayes_kw = _temporal_inference_kwargs(inference)
1396        if isinstance(data, EventFrame):
1397            if discovery is not None:
1398                if isinstance(discovery, JPCMCIPlus):
1399                    raise TypeError(
1400                        "EventFrame does not support discovery=JPCMCIPlus(...); "
1401                        "use MultiEnvFrame or PanelFrame for multi-environment discovery"
1402                    )
1403                if isinstance(discovery, DbnPosterior):
1404                    if not isinstance(inference, Bayesian):
1405                        raise TypeError(
1406                            "EventFrame discovery=DbnPosterior(...) requires inference=Bayesian(...)"
1407                        )
1408                elif not isinstance(discovery, (PCMCI, PCMCIPlus, LPCMCI, RPCMCI)):
1409                    raise TypeError(
1410                        f"EventFrame discovery expects PCMCI/PCMCIPlus/LPCMCI/RPCMCI/DbnPosterior, "
1411                        f"got {type(discovery)!r}"
1412                    )
1413                cfg = _discovery_algorithm(discovery)
1414                raw = _analyze_events(
1415                    data.names,
1416                    data.columns,
1417                    data.event_times_ns.tolist(),
1418                    data.align_interval_ns,
1419                    [],  # discovery path ignores edges
1420                    query.treatment,
1421                    query.outcome,
1422                    treatment_lag=query.treatment_lag,
1423                    horizon_steps=query.horizon_steps,
1424                    active_level=query.active_level,
1425                    policy=policy,
1426                    **bayes_kw,
1427                    refute=refute,
1428                    validators=list(validators) if validators is not None else None,
1429                    seed=seed,
1430                    bootstrap=bootstrap,
1431                    threads=threads,
1432                    algorithm=cfg["algorithm"],
1433                    max_lag=cfg.get("max_lag", 1),
1434                    alpha=cfg.get("alpha", 0.05),
1435                    fdr=cfg.get("fdr", True),
1436                    accept_discovered=accept_discovered,
1437                    regimes=list(regimes) if regimes is not None else None,
1438                    **{
1439                        k: cfg[k]
1440                        for k in ("n_chains", "n_warmup", "mcmc_draws", "force_mcmc", "ci")
1441                        if k in cfg
1442                    },
1443                )
1444                return _wrap_temporal(raw)
1445            lagged = _lagged_edges(graph)  # type: ignore[arg-type]
1446            raw = _analyze_events(
1447                data.names,
1448                data.columns,
1449                data.event_times_ns.tolist(),
1450                data.align_interval_ns,
1451                lagged,
1452                query.treatment,
1453                query.outcome,
1454                treatment_lag=query.treatment_lag,
1455                horizon_steps=query.horizon_steps,
1456                active_level=query.active_level,
1457                policy=policy,
1458                **bayes_kw,
1459                refute=refute,
1460                validators=list(validators) if validators is not None else None,
1461                seed=seed,
1462                bootstrap=bootstrap,
1463                threads=threads,
1464            )
1465            return _wrap_temporal(raw)
1466        if isinstance(data, PanelFrame):
1467            if discovery is not None:
1468                if isinstance(discovery, JPCMCIPlus):
1469                    cfg = _discovery_algorithm(discovery)
1470                    raw = _analyze_panel_discover(
1471                        data.names,
1472                        data.unit_columns,
1473                        data.unit_ids,
1474                        query.treatment,
1475                        query.outcome,
1476                        max_lag=cfg["max_lag"],
1477                        alpha=cfg["alpha"],
1478                        fdr=cfg["fdr"],
1479                        accept_discovered=accept_discovered,
1480                        treatment_lag=query.treatment_lag,
1481                        horizon_steps=query.horizon_steps,
1482                        active_level=query.active_level,
1483                        policy=policy,
1484                        **bayes_kw,
1485                        refute=refute,
1486                        validators=list(validators) if validators is not None else None,
1487                        seed=seed,
1488                        bootstrap=bootstrap,
1489                        threads=threads,
1490                        context_names=cfg["context_names"],
1491                        include_space_dummy=cfg["include_space_dummy"],
1492                        include_time_dummy=cfg["include_time_dummy"],
1493                        space_dummy_ci=cfg["space_dummy_ci"]
1494                        in ("multivariate", "multivariate_block", "block", True),
1495                        time_dummy_encoding=cfg["time_dummy_encoding"],
1496                        time_dummy_ci=cfg["time_dummy_ci"]
1497                        in ("multivariate", "multivariate_block", "block", True),
1498                    )
1499                    return _wrap_temporal(raw)
1500                if isinstance(discovery, (PCMCI, PCMCIPlus, LPCMCI)):
1501                    cfg = _discovery_algorithm(discovery)
1502                    # Pooled-units discovery: treat panel as multi-env without JPCMCI+ context.
1503                    raw = _analyze_panel_discover(
1504                        data.names,
1505                        data.unit_columns,
1506                        data.unit_ids,
1507                        query.treatment,
1508                        query.outcome,
1509                        max_lag=cfg["max_lag"],
1510                        alpha=cfg["alpha"],
1511                        fdr=cfg["fdr"],
1512                        accept_discovered=accept_discovered,
1513                        treatment_lag=query.treatment_lag,
1514                        horizon_steps=query.horizon_steps,
1515                        active_level=query.active_level,
1516                        policy=policy,
1517                        **bayes_kw,
1518                        refute=refute,
1519                        validators=list(validators) if validators is not None else None,
1520                        seed=seed,
1521                        bootstrap=bootstrap,
1522                        threads=threads,
1523                        algorithm=cfg["algorithm"],
1524                    )
1525                    return _wrap_temporal(raw)
1526                raise TypeError(
1527                    "PanelFrame discovery supports JPCMCIPlus, PCMCI, PCMCIPlus, or LPCMCI"
1528                )
1529            lagged = _lagged_edges(graph)  # type: ignore[arg-type]
1530            raw = _analyze_panel(
1531                data.names,
1532                data.unit_columns,
1533                data.unit_ids,
1534                lagged,
1535                query.treatment,
1536                query.outcome,
1537                treatment_lag=query.treatment_lag,
1538                horizon_steps=query.horizon_steps,
1539                active_level=query.active_level,
1540                policy=policy,
1541                **bayes_kw,
1542                refute=refute,
1543                validators=list(validators) if validators is not None else None,
1544                seed=seed,
1545                bootstrap=bootstrap,
1546                threads=threads,
1547            )
1548            return _wrap_temporal(raw)
1549        if isinstance(data, MultiEnvFrame):
1550            if discovery is None or not isinstance(discovery, JPCMCIPlus):
1551                raise TypeError(
1552                    "MultiEnvFrame requires discovery=JPCMCIPlus(...)"
1553                )
1554            cfg = _discovery_algorithm(discovery)
1555            raw = _analyze_temporal_discover(
1556                data.names,
1557                data.env_columns[0],
1558                query.treatment,
1559                query.outcome,
1560                algorithm="jpcmci_plus",
1561                max_lag=cfg["max_lag"],
1562                alpha=cfg["alpha"],
1563                fdr=cfg["fdr"],
1564                accept_discovered=accept_discovered,
1565                treatment_lag=query.treatment_lag,
1566                horizon_steps=query.horizon_steps,
1567                active_level=query.active_level,
1568                policy=policy,
1569                **bayes_kw,
1570                seed=seed,
1571                bootstrap=bootstrap,
1572                threads=threads,
1573                env_columns=data.env_columns,
1574                context_names=cfg["context_names"],
1575                include_space_dummy=cfg["include_space_dummy"],
1576                include_time_dummy=cfg["include_time_dummy"],
1577                space_dummy_ci=cfg["space_dummy_ci"],
1578                time_dummy_encoding=cfg["time_dummy_encoding"],
1579                time_dummy_ci=cfg["time_dummy_ci"],
1580                ci=cfg.get("ci"),
1581            )
1582            return _wrap_temporal(raw)
1583        if discovery is not None:
1584            if isinstance(discovery, DbnPosterior):
1585                if not isinstance(inference, Bayesian):
1586                    raise TypeError(
1587                        "discovery=DbnPosterior(...) requires inference=Bayesian(...) "
1588                        "for temporal effect mixture"
1589                    )
1590                cfg = _discovery_algorithm(discovery)
1591                names, columns = as_columns(data)  # type: ignore[arg-type]
1592                raw = _analyze_temporal_discover(
1593                    names,
1594                    columns,
1595                    query.treatment,
1596                    query.outcome,
1597                    algorithm="dbn_posterior",
1598                    max_lag=cfg["max_lag"],
1599                    accept_discovered=accept_discovered,
1600                    treatment_lag=query.treatment_lag,
1601                    horizon_steps=query.horizon_steps,
1602                    active_level=query.active_level,
1603                    policy=policy,
1604                    **bayes_kw,
1605                    n_chains=cfg["n_chains"],
1606                    n_warmup=cfg["n_warmup"],
1607                    mcmc_draws=cfg["mcmc_draws"],
1608                    force_mcmc=cfg["force_mcmc"],
1609                    seed=seed,
1610                    bootstrap=bootstrap,
1611                    threads=threads,
1612                )
1613                return _wrap_temporal(raw)
1614            if not isinstance(discovery, _TEMPORAL_DISCOVERY):
1615                raise TypeError(
1616                    f"temporal discovery expects PCMCI-family or DbnPosterior, got {type(discovery)!r}"
1617                )
1618            cfg = _discovery_algorithm(discovery)
1619            algo = cfg["algorithm"]
1620            if algo == "jpcmci_plus":
1621                if not isinstance(data, Sequence) or isinstance(data, (str, bytes, Mapping)):
1622                    raise TypeError(
1623                        "discovery=JPCMCIPlus(...) requires data as a sequence of "
1624                        "environment mappings/DataFrames"
1625                    )
1626                names, env_columns = as_multi_env_columns(data)
1627                raw = _analyze_temporal_discover(
1628                    names,
1629                    env_columns[0],
1630                    query.treatment,
1631                    query.outcome,
1632                    algorithm=algo,
1633                    max_lag=cfg["max_lag"],
1634                    alpha=cfg["alpha"],
1635                    fdr=cfg["fdr"],
1636                    accept_discovered=accept_discovered,
1637                    treatment_lag=query.treatment_lag,
1638                    horizon_steps=query.horizon_steps,
1639                    active_level=query.active_level,
1640                    policy=policy,
1641                    **bayes_kw,
1642                    seed=seed,
1643                    bootstrap=bootstrap,
1644                    threads=threads,
1645                    env_columns=env_columns,
1646                    context_names=cfg["context_names"],
1647                    include_space_dummy=cfg["include_space_dummy"],
1648                    include_time_dummy=cfg["include_time_dummy"],
1649                    space_dummy_ci=cfg["space_dummy_ci"],
1650                    time_dummy_encoding=cfg["time_dummy_encoding"],
1651                    time_dummy_ci=cfg["time_dummy_ci"],
1652                    ci=cfg.get("ci"),
1653                )
1654                return _wrap_temporal(raw)
1655            if algo == "rpcmci":
1656                if regimes is None:
1657                    raise ValueError("discovery=RPCMCI(...) requires regimes=[…] labels")
1658                names, columns = as_columns(data)  # type: ignore[arg-type]
1659                raw = _analyze_temporal_discover(
1660                    names,
1661                    columns,
1662                    query.treatment,
1663                    query.outcome,
1664                    algorithm=algo,
1665                    max_lag=cfg["max_lag"],
1666                    alpha=cfg["alpha"],
1667                    fdr=cfg["fdr"],
1668                    accept_discovered=accept_discovered,
1669                    treatment_lag=query.treatment_lag,
1670                    horizon_steps=query.horizon_steps,
1671                    active_level=query.active_level,
1672                    policy=policy,
1673                    **bayes_kw,
1674                    seed=seed,
1675                    bootstrap=bootstrap,
1676                    threads=threads,
1677                    regimes=list(regimes),
1678                    ci=cfg.get("ci"),
1679                )
1680                return _wrap_temporal(raw)
1681            names, columns = as_columns(data)  # type: ignore[arg-type]
1682            raw = _analyze_temporal_discover(
1683                names,
1684                columns,
1685                query.treatment,
1686                query.outcome,
1687                algorithm=algo,
1688                max_lag=cfg["max_lag"],
1689                alpha=cfg["alpha"],
1690                fdr=cfg["fdr"],
1691                accept_discovered=accept_discovered,
1692                treatment_lag=query.treatment_lag,
1693                horizon_steps=query.horizon_steps,
1694                active_level=query.active_level,
1695                policy=policy,
1696                **bayes_kw,
1697                seed=seed,
1698                bootstrap=bootstrap,
1699                threads=threads,
1700                ci=cfg.get("ci"),
1701            )
1702            return _wrap_temporal(raw)
1703        names, columns = as_columns(data)  # type: ignore[arg-type]
1704        if isinstance(graph, TemporalPag):
1705            raw = _analyze_temporal_pag(
1706                names,
1707                columns,
1708                graph,
1709                query.treatment,
1710                query.outcome,
1711                treatment_lag=query.treatment_lag,
1712                horizon_steps=query.horizon_steps,
1713                active_level=query.active_level,
1714                policy=policy,
1715                **bayes_kw,
1716                refute=refute,
1717                validators=list(validators) if validators is not None else None,
1718                seed=seed,
1719                bootstrap=bootstrap,
1720                threads=threads,
1721            )
1722            return _wrap_temporal(raw)
1723        if isinstance(graph, TemporalCpdag):
1724            try:
1725                graph = graph.try_into_temporal_dag()
1726            except Exception as exc:  # noqa: BLE001 — surface orientation failures
1727                raise ValueError(
1728                    "TemporalCpdag has undirected/conflict marks; orient edges "
1729                    "(try_into_temporal_dag) before analyze, or use discovery review"
1730                ) from exc
1731        lagged = _lagged_edges(graph)  # type: ignore[arg-type]
1732        raw = _analyze_temporal(
1733            names,
1734            columns,
1735            lagged,
1736            query.treatment,
1737            query.outcome,
1738            treatment_lag=query.treatment_lag,
1739            horizon_steps=query.horizon_steps,
1740            active_level=query.active_level,
1741            policy=policy,
1742            **bayes_kw,
1743            refute=refute,
1744            validators=list(validators) if validators is not None else None,
1745            seed=seed,
1746            bootstrap=bootstrap,
1747            threads=threads,
1748        )
1749        return _wrap_temporal(raw)
1750
1751    raise TypeError(f"unsupported query type: {type(query)!r}")

Identify then estimate a causal effect.

Parameters

data: Mapping of column name → 1-d float array, a pandas DataFrame, Arrow CDI exporters (PyArrow columns / table), or a antecedent.data frame (EventFrame / PanelFrame / MultiEnvFrame). For discovery=JPCMCIPlus(...), pass a sequence of environment frames or a MultiEnvFrame. query: AverageEffect, PulseEffect / SustainedEffect, InterventionalDistribution, PathSpecificEffect, MediationEffect, Counterfactual, or TemporalMediationEffect. graph: Dag / Cpdag / Pag / Admg / TemporalDag / TemporalCpdag / TemporalPag, or an edge list. Lagged edges (from, from_lag, to, to_lag) are required for temporal queries without discovery. Fully oriented CPDAGs run as DAGs; incomplete CPDAGs require review. ADMGs without bidirected edges coerce to DAGs; ADMGs with latents use general ID + functional effect. discovery: Static: PC / GES / LiNGAM / NOTEARS / FCI / RFCI. Temporal: PCMCI / PCMCIPlus / LPCMCI / JPCMCIPlus / RPCMCI. One-shot script convenience — discovery runs at compile time. For interactive / spreadsheet estimate clicks, discover once into antecedent.AcceptedGraph (or hold a reviewed graph) and pass graph= with latency="interactive" instead. Combining discovery= with latency="interactive" raises CausalUnsupportedError. latency: Optional compute tier (interactive / standard / report). Maps to known-equivalent bootstrap / refute / draws; explicit bootstrap= / refute= always win. Interactive refuses inline discovery= (artifact-first UX). cancel: Optional CancellationToken from antecedent._native. on_progress: Optional (fraction: float, stage: str) -> None callback. on_stage: Optional (stage: str, payload: dict) -> None progressive stage callback (identify → estimate_point → uncertainty → validate). return_posterior_artifact: When True and inference is Bayesian, attach full posterior draw bytes on result.posterior.artifact (for download / sequential-prior hydrate). Default False: UI summaries only.

def analyze_many( data: Union[Mapping[str, Any], Any], *, graph: Union[Dag, Sequence[tuple[str, str]]], queries: Sequence[AverageEffect], identifier: str | None = None, estimator: str | None = None, refute: Union[bool, Literal['full', 'placebo', 'none', 'cheap']] = True, seed: int = 1, bootstrap: int | None = None, threads: int = 1, latency: Optional[Literal['interactive', 'standard', 'report']] = None) -> list[AnalysisResult]:
886def analyze_many(
887    data: Mapping[str, Any] | Any,
888    *,
889    graph: Dag | Sequence[tuple[str, str]],
890    queries: Sequence[AverageEffect],
891    identifier: str | None = None,
892    estimator: str | None = None,
893    refute: bool | Literal["full", "placebo", "none", "cheap"] = True,
894    seed: int = 1,
895    bootstrap: int | None = None,
896    threads: int = 1,
897    latency: Literal["interactive", "standard", "report"] | None = None,
898) -> list[AnalysisResult]:
899    """Estimate many average effects on one shared table ingest.
900
901    Parameters
902    ----------
903    data:
904        Column mapping / DataFrame (ingested once).
905    graph:
906        Static DAG or edge list shared by every query.
907    queries:
908        Non-empty sequence of ``AverageEffect`` queries.
909    """
910    if not queries:
911        raise ValueError("analyze_many requires at least one query")
912    if not all(isinstance(q, AverageEffect) for q in queries):
913        raise TypeError("analyze_many currently supports AverageEffect queries only")
914    bootstrap, refute = _resolve_latency_budget(latency, bootstrap, refute)
915    names, columns = as_columns(data)  # type: ignore[arg-type]
916    edges = _static_edges(graph)  # type: ignore[arg-type]
917    specs = [
918        (q.treatment, q.outcome, float(q.control_level), float(q.active_level))
919        for q in queries
920    ]
921    kwargs: dict[str, Any] = dict(
922        identifier=identifier,
923        estimator=estimator,
924        refute=refute,
925        seed=seed,
926        bootstrap=bootstrap,
927        threads=threads,
928    )
929    if latency is not None:
930        kwargs["latency"] = latency
931    raws = _analyze_ate_many(names, columns, edges, specs, **kwargs)
932    return [_wrap_ate(r) for r in raws]

Estimate many average effects on one shared table ingest.

Parameters

data: Column mapping / DataFrame (ingested once). graph: Static DAG or edge list shared by every query. queries: Non-empty sequence of AverageEffect queries.

def identify( *, graph: Union[Dag, Sequence[tuple[str, str]]], query: AverageEffect, names: Optional[Sequence[str]] = None, identifier: str | Identifier | None = None) -> IdentifyResult:
944def identify(
945    *,
946    graph: Dag | Sequence[tuple[str, str]],
947    query: AverageEffect,
948    names: Sequence[str] | None = None,
949    identifier: str | Identifier | None = None,
950) -> IdentifyResult:
951    """Identify without estimating.
952
953    Pass ``names`` when ``graph`` is an edge list (variable order). With a
954    ``Dag``, names are taken from ``graph.nodes()``.
955    """
956    if isinstance(identifier, Identifier):
957        identifier = str(identifier)
958    if isinstance(graph, Dag):
959        node_names = list(graph.nodes())
960        edges = list(graph.edges())
961    else:
962        if names is None:
963            raise ValueError("identify(edge_list) requires names=")
964        node_names = list(names)
965        edges = list(graph)
966    status, method, adjustment = _identify_ate(
967        node_names,
968        edges,
969        query.treatment,
970        query.outcome,
971        identifier=identifier,
972    )
973    return IdentifyResult(status=status, method=method, adjustment_set=list(adjustment))

Identify without estimating.

Pass names when graph is an edge list (variable order). With a Dag, names are taken from graph.nodes().

def fit_gcm(names, columns, edges, *, threads=1):

Fit a linear-Gaussian GCM; return a reusable [FittedGcm].

class FittedGcm:

Fitted GCM retained across Python calls (fit once, sample / attribute many times).

Model, data, and names are [Arc]-shared so GIL-detached methods clone handles instead of deep-copying the fitted session on every click.

def mechanism_kinds(self, /):

Selected mechanism family id per variable name.

def sample_do(self, /, interventions, n, *, seed=0, threads=1):

Interventional ancestral sample under hard do values.

def counterfactual_ite(self, /, treatment, outcome, active, control, *, seed=0, threads=1):

Unit-level ITE under hard interventions on treatment.

def attribute_path_specific( self, /, treatment, outcome, *, path_nodes=None, max_paths=64, max_len=16, seed=0, threads=1):
def attribute_paths( self, /, sources, outcome, *, max_paths=64, max_len=16, seed=0, threads=1):
def attribute_distribution_change( self, /, outcome, baseline_start, baseline_end, comparison_start, comparison_end, *, n_samples=500, seed=0, threads=1):
def attribute_distribution_change_robust( self, /, outcome, baseline_start, baseline_end, comparison_start, comparison_end, *, n_samples=500, seed=0, threads=1):
def attribute_structure_change( self, /, comparison_edges, outcome, baseline_start, baseline_end, comparison_start, comparison_end, *, n_samples=500, seed=0, threads=1):
def attribute_unit_change(self, /, outcome, *, max_units=0, seed=0, threads=1):
def attribute_feature_relevance(self, /, outcome, *, delta=1.0, n_samples=200, seed=0, threads=1):
def anomaly_attribution(self, /, outcomes, *, max_units=0):
def mechanism_change_detection( self, /, baseline_start, baseline_end, comparison_start, comparison_end, *, seed=0, threads=1):
def rank_root_causes(self, /, attribution, *, seed=0, threads=1):
n_assignments
names
class CausalAttributionError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalCompileError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalCounterfactualError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalDataError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalDesignError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalDiscoveryError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalError(builtins.Exception):

Common base class for all non-exit exceptions.

class CausalEstimateError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalGraphError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalIdentifyError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalModelError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalResourceError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalReviewError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalSerializationError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalStateError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalUnsupportedError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CausalCancelledError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class CancellationToken:

Cooperative cancellation token shared with a running analysis.

def cancel(self, /):
def is_cancelled(self, /):
class CausalValidateError(antecedent.CausalError):

Common base class for all non-exit exceptions.

class AnomalyScores:

Anomaly scores for one outcome.

n_units
outcome
mean_score
class ArrowLoadInfo:

Result of the conversion probe (same Arrow→tabular path as analyze/discover).

diagnostic_count
column_names

Schema names after library-owned ingestion (proves the batch was parsed).

bytes_borrowed
row_count
bytes_copied
column_count
class ChangeAttributionResult:

Change-attribution summary (components and optional path breakdown).

path_breakdown
total_change
contributions
@dataclass(frozen=True)
class CompatibilityReport:
438@dataclass(frozen=True)
439class CompatibilityReport:
440    """Result of checking one source against a target design."""
441
442    status: Literal["compatible", "partial", "rejected"]
443    artifact_id: str
444    missing: tuple[str, ...] = ()
445    mappable: tuple[str, ...] = ()
446    reason: Mapping[str, Any] | None = None
447
448    @property
449    def is_usable(self) -> bool:
450        return self.status in ("compatible", "partial")
451
452    @classmethod
453    def from_dict(cls, d: Mapping[str, Any]) -> CompatibilityReport:
454        return cls(
455            status=d["status"],
456            artifact_id=d["artifact_id"],
457            missing=tuple(d.get("missing") or ()),
458            mappable=tuple(d.get("mappable") or ()),
459            reason=d.get("reason"),
460        )
461
462    def to_dict(self) -> dict[str, Any]:
463        out: dict[str, Any] = {
464            "status": self.status,
465            "artifact_id": self.artifact_id,
466        }
467        if self.missing:
468            out["missing"] = list(self.missing)
469        if self.mappable:
470            out["mappable"] = list(self.mappable)
471        if self.reason is not None:
472            out["reason"] = dict(self.reason)
473        return out

Result of checking one source against a target design.

CompatibilityReport( status: Literal['compatible', 'partial', 'rejected'], artifact_id: str, missing: tuple[str, ...] = (), mappable: tuple[str, ...] = (), reason: Optional[Mapping[str, Any]] = None)
status: Literal['compatible', 'partial', 'rejected']
artifact_id: str
missing: tuple[str, ...] = ()
mappable: tuple[str, ...] = ()
reason: Optional[Mapping[str, Any]] = None
is_usable: bool
448    @property
449    def is_usable(self) -> bool:
450        return self.status in ("compatible", "partial")
@classmethod
def from_dict(cls, d: Mapping[str, Any]) -> CompatibilityReport:
452    @classmethod
453    def from_dict(cls, d: Mapping[str, Any]) -> CompatibilityReport:
454        return cls(
455            status=d["status"],
456            artifact_id=d["artifact_id"],
457            missing=tuple(d.get("missing") or ()),
458            mappable=tuple(d.get("mappable") or ()),
459            reason=d.get("reason"),
460        )
def to_dict(self) -> dict[str, typing.Any]:
462    def to_dict(self) -> dict[str, Any]:
463        out: dict[str, Any] = {
464            "status": self.status,
465            "artifact_id": self.artifact_id,
466        }
467        if self.missing:
468            out["missing"] = list(self.missing)
469        if self.mappable:
470            out["mappable"] = list(self.mappable)
471        if self.reason is not None:
472            out["reason"] = dict(self.reason)
473        return out
@dataclass(frozen=True)
class CiScreenedPosterior:
183@dataclass(frozen=True)
184class CiScreenedPosterior:
185    alpha: float = 0.05
186    fdr: bool = True
187    ci: str = "parcorr"
188    max_cond_size: int = 2
189    soft_weight: Literal["none", "bayes_factor", "posterior_dependence"] = "none"
190    n_chains: int = 2
191    n_warmup: int = 300
192    n_draws: int = 600
193    thin: int = 1
194    kind: Literal["ci_screened_posterior"] = "ci_screened_posterior"
CiScreenedPosterior( alpha: float = 0.05, fdr: bool = True, ci: str = 'parcorr', max_cond_size: int = 2, soft_weight: Literal['none', 'bayes_factor', 'posterior_dependence'] = 'none', n_chains: int = 2, n_warmup: int = 300, n_draws: int = 600, thin: int = 1, kind: Literal['ci_screened_posterior'] = 'ci_screened_posterior')
alpha: float = 0.05
fdr: bool = True
ci: str = 'parcorr'
max_cond_size: int = 2
soft_weight: Literal['none', 'bayes_factor', 'posterior_dependence'] = 'none'
n_chains: int = 2
n_warmup: int = 300
n_draws: int = 600
thin: int = 1
kind: Literal['ci_screened_posterior'] = 'ci_screened_posterior'
class Contribution:

Named contribution (node or path label).

score
name
@dataclass(frozen=True)
class DbnPosterior:
197@dataclass(frozen=True)
198class DbnPosterior:
199    """Bounded-lag DBN posterior (Gaussian BIC).
200
201    Exact enumeration only when ``p ≤ 4`` and ``max_lag ≤ 2``; larger templates
202    automatically use MCMC (or set ``force_mcmc=True``).
203
204    Standalone: ``discover_dbn_posterior(...)`` returns a ``GraphPosterior``.
205    Composed: pass ``discovery=DbnPosterior(...)`` with ``inference=Bayesian(...)``
206    and ``PulseEffect``/``SustainedEffect`` to mix temporal effect draws (P1-D).
207    """
208
209    max_lag: int = 1
210    force_mcmc: bool = False
211    n_chains: int = 2
212    n_warmup: int = 200
213    n_draws: int = 400
214    kind: Literal["dbn_posterior"] = "dbn_posterior"

Bounded-lag DBN posterior (Gaussian BIC).

Exact enumeration only when p ≤ 4 and max_lag ≤ 2; larger templates automatically use MCMC (or set force_mcmc=True).

Standalone: discover_dbn_posterior(...) returns a GraphPosterior. Composed: pass discovery=DbnPosterior(...) with inference=Bayesian(...) and PulseEffect/SustainedEffect to mix temporal effect draws (P1-D).

DbnPosterior( max_lag: int = 1, force_mcmc: bool = False, n_chains: int = 2, n_warmup: int = 200, n_draws: int = 400, kind: Literal['dbn_posterior'] = 'dbn_posterior')
max_lag: int = 1
force_mcmc: bool = False
n_chains: int = 2
n_warmup: int = 200
n_draws: int = 400
kind: Literal['dbn_posterior'] = 'dbn_posterior'
class DecisionEvaluation:

Decision evaluation under a utility callback.

posterior_regret
expected_utility
chosen_action
class DesignRanking:

Full design ranking result.

early_stopped
best_index
scores
ranked
mc_samples
violations
@dataclass(frozen=True)
class DesignVariable:
31@dataclass(frozen=True)
32class DesignVariable:
33    """One variable in a prior-source design summary."""
34
35    name: str
36    role: Literal["treatment", "outcome", "covariate", "other"]

One variable in a prior-source design summary.

DesignVariable( name: str, role: Literal['treatment', 'outcome', 'covariate', 'other'])
name: str
role: Literal['treatment', 'outcome', 'covariate', 'other']
@dataclass(frozen=True)
class ExactDagPosterior:
150@dataclass(frozen=True)
151class ExactDagPosterior:
152    """Exact DAG posterior enumeration (hard limit: n ≤ 6, Gaussian BIC).
153
154    For more variables use ``OrderMcmc``, ``StructureMcmc``, or ``CiScreenedPosterior``.
155
156    Standalone: ``discover_exact_dag_posterior(...)`` returns a ``GraphPosterior``.
157    Composed: pass ``discovery=ExactDagPosterior()`` with ``inference=Bayesian(...)``
158    to ``analyze`` to mix effect draws over the graph posterior (P1-D).
159    """
160
161    kind: Literal["exact_dag_posterior"] = "exact_dag_posterior"

Exact DAG posterior enumeration (hard limit: n ≤ 6, Gaussian BIC).

For more variables use OrderMcmc, StructureMcmc, or CiScreenedPosterior.

Standalone: discover_exact_dag_posterior(...) returns a GraphPosterior. Composed: pass discovery=ExactDagPosterior() with inference=Bayesian(...) to analyze to mix effect draws over the graph posterior (P1-D).

ExactDagPosterior(kind: Literal['exact_dag_posterior'] = 'exact_dag_posterior')
kind: Literal['exact_dag_posterior'] = 'exact_dag_posterior'
@dataclass(frozen=True)
class EstimandFingerprint:
22@dataclass(frozen=True)
23class EstimandFingerprint:
24    """Query kind + treatment/outcome names for catalog matching."""
25
26    query_kind: str
27    treatment: str
28    outcome: str

Query kind + treatment/outcome names for catalog matching.

EstimandFingerprint(query_kind: str, treatment: str, outcome: str)
query_kind: str
treatment: str
outcome: str
@dataclass(frozen=True)
class FCI:
132@dataclass(frozen=True)
133class FCI:
134    alpha: float = 0.05
135    fdr: bool = True
136    ci: CiSpec = "parcorr"
137    max_cond_size: int = 2
138    kind: Literal["fci"] = "fci"
FCI( alpha: float = 0.05, fdr: bool = True, ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr', max_cond_size: int = 2, kind: Literal['fci'] = 'fci')
alpha: float = 0.05
fdr: bool = True
ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr'
max_cond_size: int = 2
kind: Literal['fci'] = 'fci'
class FeatureRelevance:

Feature relevance under interventions.

score
feature
@dataclass(frozen=True)
class GES:
105@dataclass(frozen=True)
106class GES:
107    alpha: float = 0.05
108    fdr: bool = True
109    ci: CiSpec = "parcorr"
110    max_cond_size: int = 2
111    screen_pc: bool = False
112    max_subset: int | None = None
113    kind: Literal["ges"] = "ges"
GES( alpha: float = 0.05, fdr: bool = True, ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr', max_cond_size: int = 2, screen_pc: bool = False, max_subset: int | None = None, kind: Literal['ges'] = 'ges')
alpha: float = 0.05
fdr: bool = True
ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr'
max_cond_size: int = 2
screen_pc: bool = False
max_subset: int | None = None
kind: Literal['ges'] = 'ges'
class GcmIteResult:

Result of a GCM counterfactual ITE (single boundary crossing).

unit_effects

Per-unit treatment effects (float64 NumPy array).

n_units
n_assignments
mean_ite
noise_inference
class GcmSampleResult:

Interventional samples under hard do (means + full draws).

column_means
n_nodes
n_draws
draws

Column-major draws shaped (n_nodes, n_draws).

class GraphEdge:

One marked edge from an oriented temporal CPDAG/PAG.

at_source

Endpoint mark at source: tail | arrow | circle | conflict.

source_lag
target_lag
at_target

Endpoint mark at target: tail | arrow | circle | conflict.

target
source
class GraphPosterior:

Columnar graph posterior returned by Bayesian discovery engines.

def to_weighted_samples(self, /):

Summary dict compatible with weighted-graph envelope consumers.

def edge_marginal_matrix(self, /):

Edge marginal as an n_vars × n_vars nested list (row-major from→to).

weights
rejected_invalid
names
n_vars
max_lag
converged
lagged_edge_marginals
n_graphs
edge_marginals
orientation_marginals
ess
adjacency
@dataclass(frozen=True)
class JPCMCIPlus:
76@dataclass(frozen=True)
77class JPCMCIPlus:
78    max_lag: int = 1
79    alpha: float = 0.05
80    fdr: bool = True
81    ci: CiSpec = "parcorr"
82    context_names: tuple[str, ...] = ()
83    include_space_dummy: bool = True
84    include_time_dummy: bool = False
85    space_dummy_ci: Literal["scalar", "multivariate"] = "scalar"
86    time_dummy_encoding: Literal["integer", "one_hot"] = "integer"
87    time_dummy_ci: Literal["scalar", "multivariate"] = "scalar"
88    kind: Literal["jpcmci_plus"] = "jpcmci_plus"
JPCMCIPlus( max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr', context_names: tuple[str, ...] = (), include_space_dummy: bool = True, include_time_dummy: bool = False, space_dummy_ci: Literal['scalar', 'multivariate'] = 'scalar', time_dummy_encoding: Literal['integer', 'one_hot'] = 'integer', time_dummy_ci: Literal['scalar', 'multivariate'] = 'scalar', kind: Literal['jpcmci_plus'] = 'jpcmci_plus')
max_lag: int = 1
alpha: float = 0.05
fdr: bool = True
ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr'
context_names: tuple[str, ...] = ()
include_space_dummy: bool = True
include_time_dummy: bool = False
space_dummy_ci: Literal['scalar', 'multivariate'] = 'scalar'
time_dummy_encoding: Literal['integer', 'one_hot'] = 'integer'
time_dummy_ci: Literal['scalar', 'multivariate'] = 'scalar'
kind: Literal['jpcmci_plus'] = 'jpcmci_plus'
@dataclass(frozen=True)
class LPCMCI:
67@dataclass(frozen=True)
68class LPCMCI:
69    max_lag: int = 1
70    alpha: float = 0.05
71    fdr: bool = True
72    ci: CiSpec = "parcorr"
73    kind: Literal["lpcmci"] = "lpcmci"
LPCMCI( max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr', kind: Literal['lpcmci'] = 'lpcmci')
max_lag: int = 1
alpha: float = 0.05
fdr: bool = True
ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr'
kind: Literal['lpcmci'] = 'lpcmci'
@dataclass(frozen=True)
class LiNGAM:
116@dataclass(frozen=True)
117class LiNGAM:
118    prune_threshold: float = 0.05
119    max_cond_size: int = 8
120    kind: Literal["lingam"] = "lingam"
LiNGAM( prune_threshold: float = 0.05, max_cond_size: int = 8, kind: Literal['lingam'] = 'lingam')
prune_threshold: float = 0.05
max_cond_size: int = 8
kind: Literal['lingam'] = 'lingam'
class MechanismChangeDetection:

Mechanism-change detection for one node.

changed
statistic
p_value
node
class MediationEffectsSummary:

Mediation effects summary.

mediated
direct
total
@dataclass(frozen=True)
class NOTEARS:
123@dataclass(frozen=True)
124class NOTEARS:
125    l1: float = 0.1
126    threshold: float = 0.3
127    standardize: bool = True
128    max_cond_size: int = 8
129    kind: Literal["notears"] = "notears"
NOTEARS( l1: float = 0.1, threshold: float = 0.3, standardize: bool = True, max_cond_size: int = 8, kind: Literal['notears'] = 'notears')
l1: float = 0.1
threshold: float = 0.3
standardize: bool = True
max_cond_size: int = 8
kind: Literal['notears'] = 'notears'
@dataclass(frozen=True)
class OrderMcmc:
164@dataclass(frozen=True)
165class OrderMcmc:
166    n_chains: int = 4
167    n_warmup: int = 500
168    n_draws: int = 1000
169    thin: int = 1
170    require_diagnostics_gate: bool = True
171    kind: Literal["order_mcmc"] = "order_mcmc"
OrderMcmc( n_chains: int = 4, n_warmup: int = 500, n_draws: int = 1000, thin: int = 1, require_diagnostics_gate: bool = True, kind: Literal['order_mcmc'] = 'order_mcmc')
n_chains: int = 4
n_warmup: int = 500
n_draws: int = 1000
thin: int = 1
require_diagnostics_gate: bool = True
kind: Literal['order_mcmc'] = 'order_mcmc'
@dataclass(frozen=True)
class PC:
40@dataclass(frozen=True)
41class PC:
42    alpha: float = 0.05
43    fdr: bool = True
44    ci: CiSpec = "parcorr"
45    max_cond_size: int = 2
46    kind: Literal["pc"] = "pc"
PC( alpha: float = 0.05, fdr: bool = True, ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr', max_cond_size: int = 2, kind: Literal['pc'] = 'pc')
alpha: float = 0.05
fdr: bool = True
ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr'
max_cond_size: int = 2
kind: Literal['pc'] = 'pc'
@dataclass(frozen=True)
class PCMCI:
49@dataclass(frozen=True)
50class PCMCI:
51    max_lag: int = 1
52    alpha: float = 0.05
53    fdr: bool = True
54    ci: CiSpec = "parcorr"
55    kind: Literal["pcmci"] = "pcmci"
PCMCI( max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr', kind: Literal['pcmci'] = 'pcmci')
max_lag: int = 1
alpha: float = 0.05
fdr: bool = True
ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr'
kind: Literal['pcmci'] = 'pcmci'
@dataclass(frozen=True)
class PCMCIPlus:
58@dataclass(frozen=True)
59class PCMCIPlus:
60    max_lag: int = 1
61    alpha: float = 0.05
62    fdr: bool = True
63    ci: CiSpec = "parcorr"
64    kind: Literal["pcmci_plus"] = "pcmci_plus"
PCMCIPlus( max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr', kind: Literal['pcmci_plus'] = 'pcmci_plus')
max_lag: int = 1
alpha: float = 0.05
fdr: bool = True
ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr'
kind: Literal['pcmci_plus'] = 'pcmci_plus'
class PcmciDiscoveryResult:

Coarse-grained PCMCI discovery result (single boundary crossing).

Field set is the stable Rust↔Python temporal discovery schema for .

algorithm_id
ci_tests
worker_threads
algorithm_config
graph_edges

Oriented graph body (CPDAG/PAG marks); empty for lagged-only PCMCI.

ci_name
cpdag_nodes
cpdag_undirected_edges
cpdag_directed_edges
pending_edge_count
lagged_frame_bytes
@dataclass
class PopulationRegistry:
10@dataclass
11class PopulationRegistry:
12    """Caller bindings for named row predicates and custom target weights.
13
14    Stratification estimators reject ``CustomDistribution``; use IPW/matching
15    (``estimator=\"propensity.weighting\"``) for weighted target populations.
16    """
17
18    predicates: dict[str, list[int]] = field(default_factory=dict)
19    distributions: dict[int, list[float]] = field(default_factory=dict)
20
21    def insert_predicate(self, name: str, rows: Sequence[int]) -> None:
22        self.predicates[str(name)] = [int(r) for r in rows]
23
24    def insert_distribution(self, distribution_id: int, weights: Sequence[float]) -> None:
25        self.distributions[int(distribution_id)] = [float(w) for w in weights]

Caller bindings for named row predicates and custom target weights.

Stratification estimators reject CustomDistribution; use IPW/matching (estimator="propensity.weighting") for weighted target populations.

PopulationRegistry( predicates: dict[str, list[int]] = <factory>, distributions: dict[int, list[float]] = <factory>)
predicates: dict[str, list[int]]
distributions: dict[int, list[float]]
def insert_predicate(self, name: str, rows: Sequence[int]) -> None:
21    def insert_predicate(self, name: str, rows: Sequence[int]) -> None:
22        self.predicates[str(name)] = [int(r) for r in rows]
def insert_distribution(self, distribution_id: int, weights: Sequence[float]) -> None:
24    def insert_distribution(self, distribution_id: int, weights: Sequence[float]) -> None:
25        self.distributions[int(distribution_id)] = [float(w) for w in weights]
class PosteriorArtifact:

Decoded posterior artifact for Python consumers .

unidentified_mass
q975
converged
n_draws
mean
backend_id
draws
q025
identification
sd
hessian_condition
quantity_names
class PredictSummary:

Prediction summary under intervention.

n
mean_prediction
@dataclass(frozen=True)
class EffectEnvelope:
128@dataclass(frozen=True)
129class EffectEnvelope:
130    """Mixture effect posterior over weighted graphs (PAG / graph-posterior path)."""
131
132    effect_mean: float | None
133    effect_sd: float | None
134    q025: float | None
135    q975: float | None
136    unidentified_mass: float
137    n_draws: int | None
138    backend: str | None = None

Mixture effect posterior over weighted graphs (PAG / graph-posterior path).

EffectEnvelope( effect_mean: float | None, effect_sd: float | None, q025: float | None, q975: float | None, unidentified_mass: float, n_draws: int | None, backend: str | None = None)
effect_mean: float | None
effect_sd: float | None
q025: float | None
q975: float | None
unidentified_mass: float
n_draws: int | None
backend: str | None = None
@dataclass(frozen=True)
class PredictiveCheckReport:
141@dataclass(frozen=True)
142class PredictiveCheckReport:
143    """Prior or posterior predictive check summary."""
144
145    kind: str
146    observed: float
147    predictive_mean: float
148    predictive_sd: float
149    p_value: float
150    n_sims: int

Prior or posterior predictive check summary.

PredictiveCheckReport( kind: str, observed: float, predictive_mean: float, predictive_sd: float, p_value: float, n_sims: int)
kind: str
observed: float
predictive_mean: float
predictive_sd: float
p_value: float
n_sims: int
@dataclass(frozen=True)
class ConflictSummaryView:
104@dataclass(frozen=True)
105class ConflictSummaryView:
106    """Applied external-prior alphas after conflict shrink."""
107
108    source_ids: list[str]
109    alphas_requested: list[float]
110    alphas_applied: list[float]

Applied external-prior alphas after conflict shrink.

ConflictSummaryView( source_ids: list[str], alphas_requested: list[float], alphas_applied: list[float])
source_ids: list[str]
alphas_requested: list[float]
alphas_applied: list[float]
class PriorCatalog:
494class PriorCatalog:
495    """Catalog of prior sources with compatibility filter and ranking."""
496
497    def __init__(self, sources: Sequence[PriorSource] | None = None) -> None:
498        self._sources: list[PriorSource] = list(sources or ())
499
500    @classmethod
501    def from_sources(cls, sources: Sequence[PriorSource]) -> PriorCatalog:
502        return cls(sources)
503
504    def add(self, source: PriorSource) -> None:
505        self._sources.append(source)
506
507    @property
508    def sources(self) -> tuple[PriorSource, ...]:
509        return tuple(self._sources)
510
511    def compatible_with(
512        self,
513        *,
514        query: Any,
515        variables: Sequence[str] = (),
516        tags: Mapping[str, str] | None = None,
517        allow_unidentified: bool = False,
518    ) -> list[CompatibilityReport]:
519        """Return one report per source for the target query/design."""
520        target = {
521            "estimand": asdict(_query_estimand(query)),
522            "variables": list(variables),
523            "tags": dict(tags or {}),
524            "allow_unidentified": allow_unidentified,
525        }
526        payload = []
527        for src in self._sources:
528            entry: dict[str, Any] = {"meta": src.meta.to_dict()}
529            if src.artifact is not None:
530                entry["artifact"] = bytes(src.artifact)
531            payload.append(entry)
532        raw = _filter(payload, target)
533        return [CompatibilityReport.from_dict(r) for r in raw]
534
535    def rank(
536        self,
537        reports: Sequence[CompatibilityReport],
538        scores: Mapping[str, float],
539    ) -> list[CompatibilityReport]:
540        """Stable-rank usable reports by caller similarity scores."""
541        payload = [r.to_dict() for r in reports]
542        ranked = _rank(payload, dict(scores))
543        return [CompatibilityReport.from_dict(r) for r in ranked]

Catalog of prior sources with compatibility filter and ranking.

PriorCatalog( sources: Optional[Sequence[PriorSource]] = None)
497    def __init__(self, sources: Sequence[PriorSource] | None = None) -> None:
498        self._sources: list[PriorSource] = list(sources or ())
@classmethod
def from_sources( cls, sources: Sequence[PriorSource]) -> PriorCatalog:
500    @classmethod
501    def from_sources(cls, sources: Sequence[PriorSource]) -> PriorCatalog:
502        return cls(sources)
def add(self, source: PriorSource) -> None:
504    def add(self, source: PriorSource) -> None:
505        self._sources.append(source)
sources: tuple[PriorSource, ...]
507    @property
508    def sources(self) -> tuple[PriorSource, ...]:
509        return tuple(self._sources)
def compatible_with( self, *, query: Any, variables: Sequence[str] = (), tags: Optional[Mapping[str, str]] = None, allow_unidentified: bool = False) -> list[CompatibilityReport]:
511    def compatible_with(
512        self,
513        *,
514        query: Any,
515        variables: Sequence[str] = (),
516        tags: Mapping[str, str] | None = None,
517        allow_unidentified: bool = False,
518    ) -> list[CompatibilityReport]:
519        """Return one report per source for the target query/design."""
520        target = {
521            "estimand": asdict(_query_estimand(query)),
522            "variables": list(variables),
523            "tags": dict(tags or {}),
524            "allow_unidentified": allow_unidentified,
525        }
526        payload = []
527        for src in self._sources:
528            entry: dict[str, Any] = {"meta": src.meta.to_dict()}
529            if src.artifact is not None:
530                entry["artifact"] = bytes(src.artifact)
531            payload.append(entry)
532        raw = _filter(payload, target)
533        return [CompatibilityReport.from_dict(r) for r in raw]

Return one report per source for the target query/design.

def rank( self, reports: Sequence[CompatibilityReport], scores: Mapping[str, float]) -> list[CompatibilityReport]:
535    def rank(
536        self,
537        reports: Sequence[CompatibilityReport],
538        scores: Mapping[str, float],
539    ) -> list[CompatibilityReport]:
540        """Stable-rank usable reports by caller similarity scores."""
541        payload = [r.to_dict() for r in reports]
542        ranked = _rank(payload, dict(scores))
543        return [CompatibilityReport.from_dict(r) for r in ranked]

Stable-rank usable reports by caller similarity scores.

@dataclass(frozen=True)
class PriorMapping:
39@dataclass(frozen=True)
40class PriorMapping:
41    """Declared bridge from a banked source into a target design prior.
42
43    Use the constructors for the common shapes; hydrate happens inside
44    ``Bayesian(prior_from=..., mapping=...)``.
45    """
46
47    kind: Literal[
48        "identical_coefficient_subspace",
49        "effect_functional",
50        "named_parameters",
51    ]
52    source_quantity: str | None = None
53    pairs: tuple[tuple[str, str], ...] = ()
54
55    @classmethod
56    def identical(cls) -> PriorMapping:
57        return cls(kind="identical_coefficient_subspace")
58
59    @classmethod
60    def effect_functional(cls, source_quantity: str = "ate") -> PriorMapping:
61        return cls(kind="effect_functional", source_quantity=source_quantity)
62
63    @classmethod
64    def named_parameters(cls, pairs: Sequence[tuple[str, str]]) -> PriorMapping:
65        return cls(kind="named_parameters", pairs=tuple((a, b) for a, b in pairs))
66
67    def to_dict(self) -> dict[str, Any]:
68        m: dict[str, Any] = {"kind": self.kind}
69        if self.source_quantity is not None:
70            m["source_quantity"] = self.source_quantity
71        if self.pairs:
72            m["pairs"] = [list(p) for p in self.pairs]
73        return m

Declared bridge from a banked source into a target design prior.

Use the constructors for the common shapes; hydrate happens inside Bayesian(prior_from=..., mapping=...).

PriorMapping( kind: Literal['identical_coefficient_subspace', 'effect_functional', 'named_parameters'], source_quantity: str | None = None, pairs: tuple[tuple[str, str], ...] = ())
kind: Literal['identical_coefficient_subspace', 'effect_functional', 'named_parameters']
source_quantity: str | None = None
pairs: tuple[tuple[str, str], ...] = ()
@classmethod
def identical(cls) -> PriorMapping:
55    @classmethod
56    def identical(cls) -> PriorMapping:
57        return cls(kind="identical_coefficient_subspace")
@classmethod
def effect_functional(cls, source_quantity: str = 'ate') -> PriorMapping:
59    @classmethod
60    def effect_functional(cls, source_quantity: str = "ate") -> PriorMapping:
61        return cls(kind="effect_functional", source_quantity=source_quantity)
@classmethod
def named_parameters( cls, pairs: Sequence[tuple[str, str]]) -> PriorMapping:
63    @classmethod
64    def named_parameters(cls, pairs: Sequence[tuple[str, str]]) -> PriorMapping:
65        return cls(kind="named_parameters", pairs=tuple((a, b) for a, b in pairs))
def to_dict(self) -> dict[str, typing.Any]:
67    def to_dict(self) -> dict[str, Any]:
68        m: dict[str, Any] = {"kind": self.kind}
69        if self.source_quantity is not None:
70            m["source_quantity"] = self.source_quantity
71        if self.pairs:
72            m["pairs"] = [list(p) for p in self.pairs]
73        return m
@dataclass(frozen=True)
class PriorSensitivityReport:
153@dataclass(frozen=True)
154class PriorSensitivityReport:
155    """Prior sensitivity grid (Bayesian + ``refute="full"``).
156
157    Isotropic mode fills ``scales``; external prior-bank mode fills ``alphas``
158    (multipliers on post-conflict applied α). Exactly one mode is active.
159    """
160
161    scales: list[float]
162    effect_means: list[float]
163    effect_sds: list[float]
164    alphas: list[float] | None = None

Prior sensitivity grid (Bayesian + refute="full").

Isotropic mode fills scales; external prior-bank mode fills alphas (multipliers on post-conflict applied α). Exactly one mode is active.

PriorSensitivityReport( scales: list[float], effect_means: list[float], effect_sds: list[float], alphas: list[float] | None = None)
scales: list[float]
effect_means: list[float]
effect_sds: list[float]
alphas: list[float] | None = None
@dataclass(frozen=True)
class PriorSource:
430@dataclass(frozen=True)
431class PriorSource:
432    """One catalog entry: meta plus optional posterior artifact bytes."""
433
434    meta: PriorSourceMeta
435    artifact: bytes | None = None

One catalog entry: meta plus optional posterior artifact bytes.

PriorSource( meta: PriorSourceMeta, artifact: bytes | None = None)
artifact: bytes | None = None
@dataclass(frozen=True)
class PriorSourceMeta:
364@dataclass(frozen=True)
365class PriorSourceMeta:
366    """Metadata for one prior-bank source."""
367
368    artifact_id: str
369    estimand: EstimandFingerprint
370    identification: str
371    tags: Mapping[str, str] = field(default_factory=dict)
372    design: Sequence[DesignVariable] = ()
373    contrast: str | None = None
374    provenance: Mapping[str, str] = field(default_factory=dict)
375    declared_mapping: PriorMapping | None = None
376
377    def to_dict(self) -> dict[str, Any]:
378        d: dict[str, Any] = {
379            "artifact_id": self.artifact_id,
380            "estimand": asdict(self.estimand),
381            "identification": self.identification,
382            "tags": dict(self.tags),
383            "design": [asdict(v) for v in self.design],
384        }
385        if self.contrast is not None:
386            d["contrast"] = self.contrast
387        if self.provenance:
388            d["provenance"] = dict(self.provenance)
389        if self.declared_mapping is not None:
390            d["declared_mapping"] = self.declared_mapping.to_dict()
391        return d
392
393    @classmethod
394    def from_dict(cls, d: Mapping[str, Any]) -> PriorSourceMeta:
395        est = d["estimand"]
396        mapping = None
397        if d.get("declared_mapping"):
398            md = d["declared_mapping"]
399            pairs = tuple(tuple(p) for p in md.get("pairs", ()))
400            mapping = PriorMapping(
401                kind=md["kind"],
402                source_quantity=md.get("source_quantity"),
403                pairs=pairs,
404            )
405        return cls(
406            artifact_id=d["artifact_id"],
407            estimand=EstimandFingerprint(
408                query_kind=est["query_kind"],
409                treatment=est["treatment"],
410                outcome=est["outcome"],
411            ),
412            identification=d["identification"],
413            tags=dict(d.get("tags") or {}),
414            design=tuple(
415                DesignVariable(name=row["name"], role=row["role"]) for row in d.get("design") or ()
416            ),
417            contrast=d.get("contrast"),
418            provenance=dict(d.get("provenance") or {}),
419            declared_mapping=mapping,
420        )
421
422    def to_cbor(self) -> bytes:
423        return bytes(_encode_meta(self.to_dict()))
424
425    @classmethod
426    def from_cbor(cls, raw: bytes) -> PriorSourceMeta:
427        return cls.from_dict(_decode_meta(bytes(raw)))

Metadata for one prior-bank source.

PriorSourceMeta( artifact_id: str, estimand: EstimandFingerprint, identification: str, tags: Mapping[str, str] = <factory>, design: Sequence[DesignVariable] = (), contrast: str | None = None, provenance: Mapping[str, str] = <factory>, declared_mapping: PriorMapping | None = None)
artifact_id: str
identification: str
tags: Mapping[str, str]
design: Sequence[DesignVariable] = ()
contrast: str | None = None
provenance: Mapping[str, str]
declared_mapping: PriorMapping | None = None
def to_dict(self) -> dict[str, typing.Any]:
377    def to_dict(self) -> dict[str, Any]:
378        d: dict[str, Any] = {
379            "artifact_id": self.artifact_id,
380            "estimand": asdict(self.estimand),
381            "identification": self.identification,
382            "tags": dict(self.tags),
383            "design": [asdict(v) for v in self.design],
384        }
385        if self.contrast is not None:
386            d["contrast"] = self.contrast
387        if self.provenance:
388            d["provenance"] = dict(self.provenance)
389        if self.declared_mapping is not None:
390            d["declared_mapping"] = self.declared_mapping.to_dict()
391        return d
@classmethod
def from_dict(cls, d: Mapping[str, Any]) -> PriorSourceMeta:
393    @classmethod
394    def from_dict(cls, d: Mapping[str, Any]) -> PriorSourceMeta:
395        est = d["estimand"]
396        mapping = None
397        if d.get("declared_mapping"):
398            md = d["declared_mapping"]
399            pairs = tuple(tuple(p) for p in md.get("pairs", ()))
400            mapping = PriorMapping(
401                kind=md["kind"],
402                source_quantity=md.get("source_quantity"),
403                pairs=pairs,
404            )
405        return cls(
406            artifact_id=d["artifact_id"],
407            estimand=EstimandFingerprint(
408                query_kind=est["query_kind"],
409                treatment=est["treatment"],
410                outcome=est["outcome"],
411            ),
412            identification=d["identification"],
413            tags=dict(d.get("tags") or {}),
414            design=tuple(
415                DesignVariable(name=row["name"], role=row["role"]) for row in d.get("design") or ()
416            ),
417            contrast=d.get("contrast"),
418            provenance=dict(d.get("provenance") or {}),
419            declared_mapping=mapping,
420        )
def to_cbor(self) -> bytes:
422    def to_cbor(self) -> bytes:
423        return bytes(_encode_meta(self.to_dict()))
@classmethod
def from_cbor(cls, raw: bytes) -> PriorSourceMeta:
425    @classmethod
426    def from_cbor(cls, raw: bytes) -> PriorSourceMeta:
427        return cls.from_dict(_decode_meta(bytes(raw)))
@dataclass(frozen=True)
class ComposedPrior:
167@dataclass(frozen=True)
168class ComposedPrior:
169    """Result of ``compose_external_priors``; usable as ``Bayesian(prior_from=...)``."""
170
171    mean: tuple[float, ...]
172    variance: tuple[float, ...]
173    source_ids: tuple[str, ...]
174    alphas_requested: tuple[float, ...]
175    alphas_applied: tuple[float, ...]
176    mixture_weights: tuple[float | None, ...]
177    sources: tuple[ExternalPriorSourceSpec, ...]
178    conflict: ConflictPolicy | None = None
179    conflict_p_values: tuple[float | None, ...] = ()
180    conflict_kl_values: tuple[float | None, ...] = ()
181    assumption_ids: tuple[str, ...] = ()
182    transport: TransportPolicy | None = None
183
184    def to_native_dict(self) -> dict[str, Any]:
185        d: dict[str, Any] = {
186            "mean": list(self.mean),
187            "variance": list(self.variance),
188            "source_ids": list(self.source_ids),
189            "alphas_requested": list(self.alphas_requested),
190            "alphas_applied": list(self.alphas_applied),
191            "mixture_weights": list(self.mixture_weights),
192            "sources": [s.to_dict() for s in self.sources],
193        }
194        if self.conflict is not None:
195            d["conflict"] = self.conflict.to_dict()
196        if self.conflict_p_values:
197            d["conflict_p_values"] = list(self.conflict_p_values)
198        if self.conflict_kl_values:
199            d["conflict_kl_values"] = list(self.conflict_kl_values)
200        if self.assumption_ids:
201            d["assumption_ids"] = list(self.assumption_ids)
202        if self.transport is not None:
203            d["transport"] = self.transport.to_wire()
204        return d

Result of compose_external_priors; usable as Bayesian(prior_from=...).

ComposedPrior( mean: tuple[float, ...], variance: tuple[float, ...], source_ids: tuple[str, ...], alphas_requested: tuple[float, ...], alphas_applied: tuple[float, ...], mixture_weights: tuple[float | None, ...], sources: tuple[ExternalPriorSourceSpec, ...], conflict: ConflictPolicy | None = None, conflict_p_values: tuple[float | None, ...] = (), conflict_kl_values: tuple[float | None, ...] = (), assumption_ids: tuple[str, ...] = (), transport: TransportPolicy | None = None)
mean: tuple[float, ...]
variance: tuple[float, ...]
source_ids: tuple[str, ...]
alphas_requested: tuple[float, ...]
alphas_applied: tuple[float, ...]
mixture_weights: tuple[float | None, ...]
sources: tuple[ExternalPriorSourceSpec, ...]
conflict: ConflictPolicy | None = None
conflict_p_values: tuple[float | None, ...] = ()
conflict_kl_values: tuple[float | None, ...] = ()
assumption_ids: tuple[str, ...] = ()
transport: TransportPolicy | None = None
def to_native_dict(self) -> dict[str, typing.Any]:
184    def to_native_dict(self) -> dict[str, Any]:
185        d: dict[str, Any] = {
186            "mean": list(self.mean),
187            "variance": list(self.variance),
188            "source_ids": list(self.source_ids),
189            "alphas_requested": list(self.alphas_requested),
190            "alphas_applied": list(self.alphas_applied),
191            "mixture_weights": list(self.mixture_weights),
192            "sources": [s.to_dict() for s in self.sources],
193        }
194        if self.conflict is not None:
195            d["conflict"] = self.conflict.to_dict()
196        if self.conflict_p_values:
197            d["conflict_p_values"] = list(self.conflict_p_values)
198        if self.conflict_kl_values:
199            d["conflict_kl_values"] = list(self.conflict_kl_values)
200        if self.assumption_ids:
201            d["assumption_ids"] = list(self.assumption_ids)
202        if self.transport is not None:
203            d["transport"] = self.transport.to_wire()
204        return d
@dataclass(frozen=True)
class ConflictPolicy:
 76@dataclass(frozen=True)
 77class ConflictPolicy:
 78    """Shrink external prior α from prior-PPC / KL conflict signals.
 79
 80    Applied α is ``α · 1{p > p_min} · exp(−kl_scale · kl)``, never increased.
 81    Defaults: ``p_min=0.05``, ``kl_scale=1.0``.
 82    """
 83
 84    p_min: float = 0.05
 85    kl_scale: float = 1.0
 86
 87    def to_dict(self) -> dict[str, float]:
 88        return {"p_min": self.p_min, "kl_scale": self.kl_scale}
 89
 90    def shrink_alpha(
 91        self,
 92        alpha: float,
 93        *,
 94        p_value: float | None = None,
 95        kl: float | None = None,
 96    ) -> float:
 97        return float(
 98            _shrink_alpha(
 99                float(alpha),
100                p_value=p_value,
101                kl=kl,
102                p_min=self.p_min,
103                kl_scale=self.kl_scale,
104            )
105        )

Shrink external prior α from prior-PPC / KL conflict signals.

Applied α is α · 1{p > p_min} · exp(−kl_scale · kl), never increased. Defaults: p_min=0.05, kl_scale=1.0.

ConflictPolicy(p_min: float = 0.05, kl_scale: float = 1.0)
p_min: float = 0.05
kl_scale: float = 1.0
def to_dict(self) -> dict[str, float]:
87    def to_dict(self) -> dict[str, float]:
88        return {"p_min": self.p_min, "kl_scale": self.kl_scale}
def shrink_alpha( self, alpha: float, *, p_value: float | None = None, kl: float | None = None) -> float:
 90    def shrink_alpha(
 91        self,
 92        alpha: float,
 93        *,
 94        p_value: float | None = None,
 95        kl: float | None = None,
 96    ) -> float:
 97        return float(
 98            _shrink_alpha(
 99                float(alpha),
100                p_value=p_value,
101                kl=kl,
102                p_min=self.p_min,
103                kl_scale=self.kl_scale,
104            )
105        )
@dataclass(frozen=True)
class TransportPolicy:
108@dataclass(frozen=True)
109class TransportPolicy:
110    """Explicit invariance claim for cross-population prior transfer.
111
112    Required when source and target ``population`` tags differ. Never inferred.
113    """
114
115    kind: Literal[
116        "invariant_conditional_outcome",
117        "invariant_effect_modifiers",
118        "invariant_propensity",
119    ]
120
121    @classmethod
122    def invariant_conditional_outcome(cls) -> TransportPolicy:
123        return cls(kind="invariant_conditional_outcome")
124
125    @classmethod
126    def invariant_effect_modifiers(cls) -> TransportPolicy:
127        return cls(kind="invariant_effect_modifiers")
128
129    @classmethod
130    def invariant_propensity(cls) -> TransportPolicy:
131        return cls(kind="invariant_propensity")
132
133    def to_wire(self) -> str:
134        return self.kind

Explicit invariance claim for cross-population prior transfer.

Required when source and target population tags differ. Never inferred.

TransportPolicy( kind: Literal['invariant_conditional_outcome', 'invariant_effect_modifiers', 'invariant_propensity'])
kind: Literal['invariant_conditional_outcome', 'invariant_effect_modifiers', 'invariant_propensity']
@classmethod
def invariant_conditional_outcome(cls) -> TransportPolicy:
121    @classmethod
122    def invariant_conditional_outcome(cls) -> TransportPolicy:
123        return cls(kind="invariant_conditional_outcome")
@classmethod
def invariant_effect_modifiers(cls) -> TransportPolicy:
125    @classmethod
126    def invariant_effect_modifiers(cls) -> TransportPolicy:
127        return cls(kind="invariant_effect_modifiers")
@classmethod
def invariant_propensity(cls) -> TransportPolicy:
129    @classmethod
130    def invariant_propensity(cls) -> TransportPolicy:
131        return cls(kind="invariant_propensity")
def to_wire(self) -> str:
133    def to_wire(self) -> str:
134        return self.kind
@dataclass(frozen=True)
class ExternalPriorSourceSpec:
148@dataclass(frozen=True)
149class ExternalPriorSourceSpec:
150    """One hydrated Gaussian coefficient prior for composition."""
151
152    id: str
153    mean: tuple[float, ...]
154    variance: tuple[float, ...]
155    weight: ExternalPriorWeight = field(default_factory=ExternalPriorWeight)
156
157    def to_dict(self) -> dict[str, Any]:
158        return {
159            "id": self.id,
160            "mean": list(self.mean),
161            "variance": list(self.variance),
162            "alpha": self.weight.alpha,
163            "mixture_weight": self.weight.mixture_weight,
164        }

One hydrated Gaussian coefficient prior for composition.

ExternalPriorSourceSpec( id: str, mean: tuple[float, ...], variance: tuple[float, ...], weight: ExternalPriorWeight = <factory>)
id: str
mean: tuple[float, ...]
variance: tuple[float, ...]
def to_dict(self) -> dict[str, typing.Any]:
157    def to_dict(self) -> dict[str, Any]:
158        return {
159            "id": self.id,
160            "mean": list(self.mean),
161            "variance": list(self.variance),
162            "alpha": self.weight.alpha,
163            "mixture_weight": self.weight.mixture_weight,
164        }
@dataclass(frozen=True)
class ExternalPriorWeight:
137@dataclass(frozen=True)
138class ExternalPriorWeight:
139    """Per-source power-prior α and optional mixture weight."""
140
141    alpha: float = 1.0
142    mixture_weight: float | None = None
143
144    def to_dict(self) -> dict[str, Any]:
145        return {"alpha": self.alpha, "mixture_weight": self.mixture_weight}

Per-source power-prior α and optional mixture weight.

ExternalPriorWeight(alpha: float = 1.0, mixture_weight: float | None = None)
alpha: float = 1.0
mixture_weight: float | None = None
def to_dict(self) -> dict[str, typing.Any]:
144    def to_dict(self) -> dict[str, Any]:
145        return {"alpha": self.alpha, "mixture_weight": self.mixture_weight}
POPULATION_TAG_KEY = 'population'
def compose_external_priors( sources: Sequence[ExternalPriorSourceSpec], weights: Union[Sequence[ExternalPriorWeight], Sequence[float], NoneType] = None, *, baseline: tuple[typing.Sequence[float], typing.Sequence[float]] | None = None, conflict: ConflictPolicy | None = None, conflict_signals: Optional[Sequence[Mapping[str, float | None]]] = None, transport: TransportPolicy | None = None, target_population: str | None = None, source_populations: Optional[Sequence[str | None]] = None, prior_sources: Optional[Sequence[PriorSource]] = None, unit_effects: Optional[Sequence[float]] = None, transport_weights: Optional[Sequence[float]] = None, coef_index: int | None = None) -> ComposedPrior:
239def compose_external_priors(
240    sources: Sequence[ExternalPriorSourceSpec],
241    weights: Sequence[ExternalPriorWeight] | Sequence[float] | None = None,
242    *,
243    baseline: tuple[Sequence[float], Sequence[float]] | None = None,
244    conflict: ConflictPolicy | None = None,
245    conflict_signals: Sequence[Mapping[str, float | None]] | None = None,
246    transport: TransportPolicy | None = None,
247    target_population: str | None = None,
248    source_populations: Sequence[str | None] | None = None,
249    prior_sources: Sequence[PriorSource] | None = None,
250    unit_effects: Sequence[float] | None = None,
251    transport_weights: Sequence[float] | None = None,
252    coef_index: int | None = None,
253) -> ComposedPrior:
254    """Compose external Gaussian priors with power-prior / mixture weights.
255
256    Parameters
257    ----------
258    sources:
259        Hydrated coefficient priors (same dimension as the target design).
260    weights:
261        Per-source ``ExternalPriorWeight`` or mixture floats (α kept from source).
262    baseline:
263        ``(mean, variance)`` for leftover / precision-add baseline. Defaults to
264        weakly informative isotropic ``V0=100`` (scale 10) at the source dimension.
265    conflict:
266        Optional policy; with ``conflict_signals``, shrinks α offline. When used
267        as ``Bayesian(prior_from=...)``, the same policy re-evaluates after data bind.
268    conflict_signals:
269        Optional per-source ``{"p_value": ..., "kl": ...}`` for offline shrink.
270    transport:
271        Required when ``source_populations`` differ from ``target_population``.
272    target_population:
273        Target analysis population tag (caller convention; matches meta tag
274        ``population``).
275    source_populations:
276        Per-source population tags (same length as ``sources``). When omitted,
277        tags are read from ``prior_sources`` when that argument is provided.
278    prior_sources:
279        Catalog entries aligned with ``sources``; used to auto-fill
280        ``source_populations`` from ``meta.tags["population"]`` when
281        ``source_populations`` is unset.
282    unit_effects / transport_weights:
283        Optional unit-level effect contributions and target-alignment weights
284        for importance-weighted moment adjustment.
285    coef_index:
286        Coefficient index rewritten under reweight (default: last).
287    """
288    srcs = _normalize_weights(sources, weights)
289    if not srcs:
290        raise ValueError("compose_external_priors requires at least one source")
291    n = len(srcs[0].mean)
292    if baseline is None:
293        baseline_mean = [0.0] * n
294        baseline_var = [100.0] * n
295    else:
296        baseline_mean = list(baseline[0])
297        baseline_var = list(baseline[1])
298    payload = [s.to_dict() for s in srcs]
299    conf_dict = conflict.to_dict() if conflict is not None else None
300    sig_list = [dict(s) for s in conflict_signals] if conflict_signals is not None else None
301    pop_list = None
302    if source_populations is not None:
303        pop_list = [None if p is None else str(p) for p in source_populations]
304    elif prior_sources is not None:
305        if len(prior_sources) != len(srcs):
306            raise ValueError("prior_sources length must match sources")
307        pop_list = populations_from_prior_sources(prior_sources)
308    raw = _compose_external_priors(
309        payload,
310        baseline_mean,
311        baseline_var,
312        conflict=conf_dict,
313        conflict_signals=sig_list,
314        transport=transport.to_wire() if transport is not None else None,
315        target_population=target_population,
316        source_populations=pop_list,
317        unit_effects=list(unit_effects) if unit_effects is not None else None,
318        transport_weights=list(transport_weights) if transport_weights is not None else None,
319        coef_index=coef_index,
320    )
321    # Prefer transported source moments when native echoed them.
322    out_sources = tuple(srcs)
323    if raw.get("sources"):
324        rebuilt: list[ExternalPriorSourceSpec] = []
325        for row in raw["sources"]:
326            rebuilt.append(
327                ExternalPriorSourceSpec(
328                    id=str(row["id"]),
329                    mean=tuple(float(x) for x in row["mean"]),
330                    variance=tuple(float(x) for x in row["variance"]),
331                    weight=ExternalPriorWeight(
332                        alpha=float(row["alpha"]),
333                        mixture_weight=(
334                            None
335                            if row.get("mixture_weight") is None
336                            else float(row["mixture_weight"])
337                        ),
338                    ),
339                )
340            )
341        out_sources = tuple(rebuilt)
342    return ComposedPrior(
343        mean=tuple(float(x) for x in raw["mean"]),
344        variance=tuple(float(x) for x in raw["variance"]),
345        source_ids=tuple(str(x) for x in raw["source_ids"]),
346        alphas_requested=tuple(float(x) for x in raw["alphas_requested"]),
347        alphas_applied=tuple(float(x) for x in raw["alphas_applied"]),
348        mixture_weights=tuple(
349            None if w is None else float(w) for w in raw["mixture_weights"]
350        ),
351        sources=out_sources,
352        conflict=conflict,
353        conflict_p_values=tuple(
354            None if x is None else float(x) for x in raw.get("conflict_p_values") or ()
355        ),
356        conflict_kl_values=tuple(
357            None if x is None else float(x) for x in raw.get("conflict_kl_values") or ()
358        ),
359        assumption_ids=tuple(str(x) for x in raw.get("assumption_ids") or ()),
360        transport=transport,
361    )

Compose external Gaussian priors with power-prior / mixture weights.

Parameters

sources: Hydrated coefficient priors (same dimension as the target design). weights: Per-source ExternalPriorWeight or mixture floats (α kept from source). baseline: (mean, variance) for leftover / precision-add baseline. Defaults to weakly informative isotropic V0=100 (scale 10) at the source dimension. conflict: Optional policy; with conflict_signals, shrinks α offline. When used as Bayesian(prior_from=...), the same policy re-evaluates after data bind. conflict_signals: Optional per-source {"p_value": ..., "kl": ...} for offline shrink. transport: Required when source_populations differ from target_population. target_population: Target analysis population tag (caller convention; matches meta tag population). source_populations: Per-source population tags (same length as sources). When omitted, tags are read from prior_sources when that argument is provided. prior_sources: Catalog entries aligned with sources; used to auto-fill source_populations from meta.tags["population"] when source_populations is unset. unit_effects / transport_weights: Optional unit-level effect contributions and target-alignment weights for importance-weighted moment adjustment. coef_index: Coefficient index rewritten under reweight (default: last).

def populations_from_prior_sources(sources: Sequence[PriorSource]) -> list[str | None]:
232def populations_from_prior_sources(
233    sources: Sequence[PriorSource],
234) -> list[str | None]:
235    """Read ``tags["population"]`` from each catalog source (``None`` if unset)."""
236    return [s.meta.tags.get(POPULATION_TAG_KEY) for s in sources]

Read tags["population"] from each catalog source (None if unset).

@dataclass(frozen=True)
class RFCI:
141@dataclass(frozen=True)
142class RFCI:
143    alpha: float = 0.05
144    fdr: bool = True
145    ci: CiSpec = "parcorr"
146    max_cond_size: int = 2
147    kind: Literal["rfci"] = "rfci"
RFCI( alpha: float = 0.05, fdr: bool = True, ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr', max_cond_size: int = 2, kind: Literal['rfci'] = 'rfci')
alpha: float = 0.05
fdr: bool = True
ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr'
max_cond_size: int = 2
kind: Literal['rfci'] = 'rfci'
@dataclass(frozen=True)
class RPCMCI:
 91@dataclass(frozen=True)
 92class RPCMCI:
 93    """Regime-PCMCI. Pass ``regimes=`` to ``analyze`` / ``discover_rpcmci`` (required).
 94
 95    Use ``two_regime_half_split(n)`` when a simple half-split label vector is enough.
 96    """
 97
 98    max_lag: int = 1
 99    alpha: float = 0.05
100    fdr: bool = True
101    ci: CiSpec = "parcorr"
102    kind: Literal["rpcmci"] = "rpcmci"

Regime-PCMCI. Pass regimes= to analyze / discover_rpcmci (required).

Use two_regime_half_split(n) when a simple half-split label vector is enough.

RPCMCI( max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr', kind: Literal['rpcmci'] = 'rpcmci')
max_lag: int = 1
alpha: float = 0.05
fdr: bool = True
ci: Union[str, Callable[..., Sequence[tuple[float, float]]]] = 'parcorr'
kind: Literal['rpcmci'] = 'rpcmci'
class RpcmciDiscoverySummary:

RPCMCI summary (typed regimes, no single-graph collapse).

n_regimes
directed_edges
algorithm
undirected_edges
regime_ids
@dataclass(frozen=True)
class StructureMcmc:
174@dataclass(frozen=True)
175class StructureMcmc:
176    n_chains: int = 4
177    n_warmup: int = 500
178    n_draws: int = 1000
179    thin: int = 1
180    kind: Literal["structure_mcmc"] = "structure_mcmc"
StructureMcmc( n_chains: int = 4, n_warmup: int = 500, n_draws: int = 1000, thin: int = 1, kind: Literal['structure_mcmc'] = 'structure_mcmc')
n_chains: int = 4
n_warmup: int = 500
n_draws: int = 1000
thin: int = 1
kind: Literal['structure_mcmc'] = 'structure_mcmc'
class TemporalCpdag:

Named temporal CPDAG over lagged variables.

def from_lagged_edges(cls, /, names, directed, undirected):
def try_into_temporal_dag(self, /):
def node_count(self, /):
class TemporalPag:

Named temporal PAG over lagged variables.

def from_marked_lagged_edges(cls, /, names, edges):
def node_count(self, /):
class CausalState:

Incremental causal analysis state (versioned events, no auto-rerun).

def stale_query_count(self, /):

Number of registered queries whose cached results are currently stale.

def stale_queries(self, /):

Raw ids of registered queries that are currently stale.

def batch_ids(self, /):

Batch ids currently retained in Python (catalog order).

def append_data(self, /, names, columns):

Append a tabular batch and retain its float64 columns.

Returns the new state version.

def replace_data(self, /, names=None, columns=None):

Replace all retained batches; optionally load one new batch.

Returns the new state version.

def get_batch(self, /, batch_id):

Fetch retained columns for batch_id as (names, list[ndarray]).

def batch_nrows(self, /, batch_id):

Row count for a retained batch.

def add_graph_evidence(self, /, evidence_id, fingerprint, bytes):

Add opaque graph evidence; returns new version.

def graph_evidence(self, /):

List (id, fingerprint, bytes) graph-evidence records.

def add_constraint(self, /, constraint_id, fingerprint):

Add a graph constraint; returns new version.

def remove_constraint(self, /, constraint_id):

Remove a graph constraint by id; returns new version.

def constraints(self, /):

List active constraint (id, fingerprint) pairs.

def update_assumption(self, /, kind):

Update / insert a named assumption (default provenance: stated / identification / assumed).

def register_average_effect(self, /, treatment, outcome):

Register a binary average-effect query; returns (version, query_id).

def record_intervention(self, /, intervention_id, fingerprint):

Record an opaque intervention; returns new version.

def refresh_results(self, /, entries):

Mark queries fresh at fingerprints: list of (query_id, fingerprint, bytes).

def ols_ensure(self, /, key, ncols):

Ensure an OLS sufficient-stat slot exists with ncols predictors.

def ols_append_row(self, /, key, row, y):

Append one OLS design row / response under key.

def ols_get(self, /, key):

Return OLS summary dict for key: {n, ncols, xtx, xty, yty}.

def cov_ensure(self, /, key, dim):

Ensure a streaming-covariance slot of dimension dim.

def cov_update(self, /, key, row):

Observe one row into streaming covariance key.

def cov_get(self, /, key):

Return streaming-cov summary {n, dim, mean, m2}.

def particle_filter_init( self, /, key, n_particles, *, a=0.9, process_std=0.3, obs_std=0.5, seed=1):

Initialize a particle filter under key.

def particle_filter_step(self, /, key, y):

Step particle filter key with observation y.

def particle_filter_get(self, /, key):

Particle-filter summary {n_obs, n_particles, particles, log_weights}.

version

Monotonic state version (starts at 0; bumps on each applied event).

data_version

Current data-catalog version.

def anomaly_attribution(names, columns, edges, outcomes, *, max_units=0):

Anomaly scores for listed outcomes.

def attribute_distribution_change( names, columns, edges, outcome, baseline_start, baseline_end, comparison_start, comparison_end, *, n_samples=500, seed=0, threads=1):

Fit GCM and attribute distribution change between two row ranges via Shapley.

def attribute_distribution_change_robust( names, columns, edges, outcome, baseline_start, baseline_end, comparison_start, comparison_end, *, n_samples=500, seed=0, threads=1):

Robust distribution-change attribution between two row ranges.

def attribute_feature_relevance( names, columns, edges, outcome, *, delta=1.0, n_samples=200, seed=0, threads=1):

Feature relevance scores for parents of outcome.

def attribute_path_specific( names, columns, edges, treatment, outcome, *, path_nodes=None, max_paths=64, max_len=16, seed=0, threads=1):

Path-specific contribution via [PathSpecificEffectQuery] / path_decompose.

def attribute_paths( names, columns, edges, sources, outcome, *, max_paths=64, max_len=16, seed=0, threads=1):

Path decomposition (all paths from sources to outcome).

def attribute_structure_change( names, columns, baseline_edges, comparison_edges, outcome, baseline_start, baseline_end, comparison_start, comparison_end, *, n_samples=500, seed=0, threads=1):

Structure-change attribution between two edge lists (parent-set Shapley).

def attribute_unit_change(names, columns, edges, outcome, *, max_units=0, seed=0, threads=1):

Unit-level change attribution.

def counterfactual_ite( names, columns, edges, treatment, outcome, active, control, *, seed=0, threads=1):

Fit a linear-Gaussian GCM and return mean ITE under hard interventions.

Crosses the Python boundary once: NumPy columns + edges in, arrays out.

def dag_from_dot(dot):

Parse DOT digraph text; return (node_count, edges).

def dag_from_gml(gml):

Parse GML digraph text; return (node_count, edges).

def dag_from_json(json):

Parse JSON DAG document; return (node_count, edges, variable_names|None).

def dag_from_networkx_adjacency(json):

Parse NetworkX adjacency JSON; return (node_count, edges).

def dag_to_dot(node_count, edges):

Emit DOT for a numeric DAG given node_count and edges.

def dag_to_gml(node_count, edges):

Emit GML for a numeric DAG.

def dag_to_json(node_count, edges, variable_names):

Emit JSON for a numeric DAG.

def dag_to_networkx_adjacency(node_count, edges, variable_names):

Emit NetworkX adjacency JSON for a numeric DAG.

def decode_model_bundle(bytes):

Decode a model bundle; return (variable_names, edges, n_mechanisms).

def decode_posterior_artifact(bytes):

Decode a serialized posterior artifact into summaries + column-major draws.

def discover_ci_screened_posterior( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', max_cond_size: int = 2, soft_weight: str = 'none', n_chains: int = 2, n_warmup: int = 300, n_draws: int = 600, thin: int = 1, threads: int = 1) -> GraphPosterior:
639def discover_ci_screened_posterior(
640    names: Any | None = None,
641    columns: Sequence[NDArray[np.float64]] | None = None,
642    *,
643    data: Any | None = None,
644    alpha: float = 0.05,
645    fdr: bool = True,
646    seed: int = 1,
647    ci: str = "parcorr",
648    max_cond_size: int = 2,
649    soft_weight: str = "none",
650    n_chains: int = 2,
651    n_warmup: int = 300,
652    n_draws: int = 600,
653    thin: int = 1,
654    threads: int = 1,
655) -> GraphPosterior:
656    n, cols = _coerce_tabular(names, columns, data=data)
657    return _discover_ci_screened_posterior(
658        n,
659        cols,
660        alpha=alpha,
661        fdr=fdr,
662        ci=ci,
663        max_cond_size=max_cond_size,
664        soft_weight=soft_weight,
665        n_chains=n_chains,
666        n_warmup=n_warmup,
667        n_draws=n_draws,
668        thin=thin,
669        seed=seed,
670        threads=threads,
671    )
def discover_dbn_posterior( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, max_lag: int = 1, force_mcmc: bool = False, n_chains: int = 2, n_warmup: int = 200, n_draws: int = 400, seed: int = 1, threads: int = 1) -> GraphPosterior:
674def discover_dbn_posterior(
675    names: Any | None = None,
676    columns: Sequence[NDArray[np.float64]] | None = None,
677    *,
678    data: Any | None = None,
679    max_lag: int = 1,
680    force_mcmc: bool = False,
681    n_chains: int = 2,
682    n_warmup: int = 200,
683    n_draws: int = 400,
684    seed: int = 1,
685    threads: int = 1,
686) -> GraphPosterior:
687    """DBN template posterior; exact only for p ≤ 4 and max_lag ≤ 2, else MCMC."""
688    n, cols = _coerce_tabular(names, columns, data=data)
689    return _discover_dbn_posterior(
690        n,
691        cols,
692        max_lag=max_lag,
693        force_mcmc=force_mcmc,
694        n_chains=n_chains,
695        n_warmup=n_warmup,
696        n_draws=n_draws,
697        seed=seed,
698        threads=threads,
699    )

DBN template posterior; exact only for p ≤ 4 and max_lag ≤ 2, else MCMC.

def discover_exact_dag_posterior( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, seed: int = 1, threads: int = 1) -> GraphPosterior:
574def discover_exact_dag_posterior(
575    names: Any | None = None,
576    columns: Sequence[NDArray[np.float64]] | None = None,
577    *,
578    data: Any | None = None,
579    seed: int = 1,
580    threads: int = 1,
581) -> GraphPosterior:
582    """Exact DAG posterior (hard limit n ≤ 6). Prefer MCMC helpers for larger graphs."""
583    n, cols = _coerce_tabular(names, columns, data=data)
584    return _discover_exact_dag_posterior(n, cols, seed=seed, threads=threads)

Exact DAG posterior (hard limit n ≤ 6). Prefer MCMC helpers for larger graphs.

def discover_jpcmci_plus( names: list[str], env_columns: Sequence[Sequence[NDArray[numpy.float64]]], *, max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', weights: list[float] | None = None, threads: int = 1, context_names: Optional[Sequence[str]] = None, include_space_dummy: bool = True, include_time_dummy: bool = False, space_dummy_ci: str = 'scalar', time_dummy_encoding: str = 'integer', time_dummy_ci: str = 'scalar') -> PcmciDiscoveryResult:
504def discover_jpcmci_plus(
505    names: list[str],
506    env_columns: Sequence[Sequence[NDArray[np.float64]]],
507    *,
508    max_lag: int = 1,
509    alpha: float = 0.05,
510    fdr: bool = True,
511    seed: int = 1,
512    ci: str = "parcorr",
513    weights: list[float] | None = None,
514    threads: int = 1,
515    context_names: Sequence[str] | None = None,
516    include_space_dummy: bool = True,
517    include_time_dummy: bool = False,
518    space_dummy_ci: str = "scalar",
519    time_dummy_encoding: str = "integer",
520    time_dummy_ci: str = "scalar",
521) -> PcmciDiscoveryResult:
522    return _discover_jpcmci_plus(
523        names,
524        [list(cols) for cols in env_columns],
525        max_lag=max_lag,
526        alpha=alpha,
527        fdr=fdr,
528        seed=seed,
529        ci=ci,
530        weights=weights,
531        threads=threads,
532        context_names=list(context_names) if context_names is not None else None,
533        include_space_dummy=include_space_dummy,
534        include_time_dummy=include_time_dummy,
535        space_dummy_ci=space_dummy_ci,
536        time_dummy_encoding=time_dummy_encoding,
537        time_dummy_ci=time_dummy_ci,
538    )
def discover_lpcmci( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', weights: list[float] | None = None, threads: int = 1) -> PcmciDiscoveryResult:
477def discover_lpcmci(
478    names: Any | None = None,
479    columns: Sequence[NDArray[np.float64]] | None = None,
480    *,
481    data: Any | None = None,
482    max_lag: int = 1,
483    alpha: float = 0.05,
484    fdr: bool = True,
485    seed: int = 1,
486    ci: str = "parcorr",
487    weights: list[float] | None = None,
488    threads: int = 1,
489) -> DiscoveryResult:
490    n, cols = _coerce_tabular(names, columns, data=data)
491    return _discover_lpcmci(
492        n,
493        cols,
494        max_lag=max_lag,
495        alpha=alpha,
496        fdr=fdr,
497        seed=seed,
498        ci=ci,
499        weights=weights,
500        threads=threads,
501    )
def discover_order_mcmc( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, n_chains: int = 4, n_warmup: int = 500, n_draws: int = 1000, thin: int = 1, require_diagnostics_gate: bool = True, seed: int = 1, threads: int = 1) -> GraphPosterior:
587def discover_order_mcmc(
588    names: Any | None = None,
589    columns: Sequence[NDArray[np.float64]] | None = None,
590    *,
591    data: Any | None = None,
592    n_chains: int = 4,
593    n_warmup: int = 500,
594    n_draws: int = 1000,
595    thin: int = 1,
596    require_diagnostics_gate: bool = True,
597    seed: int = 1,
598    threads: int = 1,
599) -> GraphPosterior:
600    n, cols = _coerce_tabular(names, columns, data=data)
601    return _discover_order_mcmc(
602        n,
603        cols,
604        n_chains=n_chains,
605        n_warmup=n_warmup,
606        n_draws=n_draws,
607        thin=thin,
608        require_diagnostics_gate=require_diagnostics_gate,
609        seed=seed,
610        threads=threads,
611    )
def discover_pc( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', max_cond_size: int = 2, threads: int = 1) -> PcmciDiscoveryResult:
273def discover_pc(
274    names: Any | None = None,
275    columns: Sequence[NDArray[np.float64]] | None = None,
276    *,
277    data: Any | None = None,
278    alpha: float = 0.05,
279    fdr: bool = True,
280    seed: int = 1,
281    ci: str = "parcorr",
282    max_cond_size: int = 2,
283    threads: int = 1,
284) -> DiscoveryResult:
285    n, cols = _coerce_tabular(names, columns, data=data)
286    return _discover_pc(
287        n,
288        cols,
289        alpha=alpha,
290        fdr=fdr,
291        seed=seed,
292        ci=ci,
293        max_cond_size=max_cond_size,
294        threads=threads,
295    )
def discover_ges( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', max_cond_size: int = 2, threads: int = 1, screen_pc: bool = False, max_subset: int | None = None) -> PcmciDiscoveryResult:
298def discover_ges(
299    names: Any | None = None,
300    columns: Sequence[NDArray[np.float64]] | None = None,
301    *,
302    data: Any | None = None,
303    alpha: float = 0.05,
304    fdr: bool = True,
305    seed: int = 1,
306    ci: str = "parcorr",
307    max_cond_size: int = 2,
308    threads: int = 1,
309    screen_pc: bool = False,
310    max_subset: int | None = None,
311) -> DiscoveryResult:
312    n, cols = _coerce_tabular(names, columns, data=data)
313    return _discover_ges(
314        n,
315        cols,
316        alpha=alpha,
317        fdr=fdr,
318        seed=seed,
319        ci=ci,
320        max_cond_size=max_cond_size,
321        threads=threads,
322        screen_pc=screen_pc,
323        max_subset=max_subset,
324    )
def discover_lingam( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, prune_threshold: float = 0.05, seed: int = 1, max_cond_size: int = 8, threads: int = 1) -> PcmciDiscoveryResult:
327def discover_lingam(
328    names: Any | None = None,
329    columns: Sequence[NDArray[np.float64]] | None = None,
330    *,
331    data: Any | None = None,
332    prune_threshold: float = 0.05,
333    seed: int = 1,
334    max_cond_size: int = 8,
335    threads: int = 1,
336) -> DiscoveryResult:
337    n, cols = _coerce_tabular(names, columns, data=data)
338    return _discover_lingam(
339        n,
340        cols,
341        prune_threshold=prune_threshold,
342        seed=seed,
343        max_cond_size=max_cond_size,
344        threads=threads,
345    )
def discover_notears( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, l1: float = 0.1, threshold: float = 0.3, standardize: bool = True, seed: int = 1, max_cond_size: int = 8, threads: int = 1) -> PcmciDiscoveryResult:
348def discover_notears(
349    names: Any | None = None,
350    columns: Sequence[NDArray[np.float64]] | None = None,
351    *,
352    data: Any | None = None,
353    l1: float = 0.1,
354    threshold: float = 0.3,
355    standardize: bool = True,
356    seed: int = 1,
357    max_cond_size: int = 8,
358    threads: int = 1,
359) -> DiscoveryResult:
360    n, cols = _coerce_tabular(names, columns, data=data)
361    return _discover_notears(
362        n,
363        cols,
364        l1=l1,
365        threshold=threshold,
366        standardize=standardize,
367        seed=seed,
368        max_cond_size=max_cond_size,
369        threads=threads,
370    )
def discover_fci( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', max_cond_size: int = 2, threads: int = 1) -> PcmciDiscoveryResult:
373def discover_fci(
374    names: Any | None = None,
375    columns: Sequence[NDArray[np.float64]] | None = None,
376    *,
377    data: Any | None = None,
378    alpha: float = 0.05,
379    fdr: bool = True,
380    seed: int = 1,
381    ci: str = "parcorr",
382    max_cond_size: int = 2,
383    threads: int = 1,
384) -> DiscoveryResult:
385    n, cols = _coerce_tabular(names, columns, data=data)
386    return _discover_fci(
387        n,
388        cols,
389        alpha=alpha,
390        fdr=fdr,
391        seed=seed,
392        ci=ci,
393        max_cond_size=max_cond_size,
394        threads=threads,
395    )
def discover_rfci( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', max_cond_size: int = 2, threads: int = 1) -> PcmciDiscoveryResult:
398def discover_rfci(
399    names: Any | None = None,
400    columns: Sequence[NDArray[np.float64]] | None = None,
401    *,
402    data: Any | None = None,
403    alpha: float = 0.05,
404    fdr: bool = True,
405    seed: int = 1,
406    ci: str = "parcorr",
407    max_cond_size: int = 2,
408    threads: int = 1,
409) -> DiscoveryResult:
410    n, cols = _coerce_tabular(names, columns, data=data)
411    return _discover_rfci(
412        n,
413        cols,
414        alpha=alpha,
415        fdr=fdr,
416        seed=seed,
417        ci=ci,
418        max_cond_size=max_cond_size,
419        threads=threads,
420    )
def discover_pcmci( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', weights: list[float] | None = None, threads: int = 1) -> PcmciDiscoveryResult:
423def discover_pcmci(
424    names: Any | None = None,
425    columns: Sequence[NDArray[np.float64]] | None = None,
426    *,
427    data: Any | None = None,
428    max_lag: int = 1,
429    alpha: float = 0.05,
430    fdr: bool = True,
431    seed: int = 1,
432    ci: str = "parcorr",
433    weights: list[float] | None = None,
434    threads: int = 1,
435) -> DiscoveryResult:
436    n, cols = _coerce_tabular(names, columns, data=data)
437    return _discover_pcmci(
438        n,
439        cols,
440        max_lag=max_lag,
441        alpha=alpha,
442        fdr=fdr,
443        seed=seed,
444        ci=ci,
445        weights=weights,
446        threads=threads,
447    )
def discover_pcmci_plus( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', weights: list[float] | None = None, threads: int = 1) -> PcmciDiscoveryResult:
450def discover_pcmci_plus(
451    names: Any | None = None,
452    columns: Sequence[NDArray[np.float64]] | None = None,
453    *,
454    data: Any | None = None,
455    max_lag: int = 1,
456    alpha: float = 0.05,
457    fdr: bool = True,
458    seed: int = 1,
459    ci: str = "parcorr",
460    weights: list[float] | None = None,
461    threads: int = 1,
462) -> DiscoveryResult:
463    n, cols = _coerce_tabular(names, columns, data=data)
464    return _discover_pcmci_plus(
465        n,
466        cols,
467        max_lag=max_lag,
468        alpha=alpha,
469        fdr=fdr,
470        seed=seed,
471        ci=ci,
472        weights=weights,
473        threads=threads,
474    )
def discover_rpcmci( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, regimes: Sequence[int], max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', weights: list[float] | None = None, threads: int = 1) -> RpcmciDiscoverySummary:
541def discover_rpcmci(
542    names: Any | None = None,
543    columns: Sequence[NDArray[np.float64]] | None = None,
544    *,
545    data: Any | None = None,
546    regimes: Sequence[int],
547    max_lag: int = 1,
548    alpha: float = 0.05,
549    fdr: bool = True,
550    seed: int = 1,
551    ci: str = "parcorr",
552    weights: list[float] | None = None,
553    threads: int = 1,
554) -> RpcmciDiscoverySummary:
555    """Run RPCMCI. ``regimes`` is required (length = series length); no silent half-split.
556
557    Call ``two_regime_half_split(len(series))`` for an explicit two-regime mid-point split.
558    """
559    n, cols = _coerce_tabular(names, columns, data=data)
560    return _discover_rpcmci(
561        n,
562        cols,
563        regimes=list(regimes),
564        max_lag=max_lag,
565        alpha=alpha,
566        fdr=fdr,
567        seed=seed,
568        ci=ci,
569        weights=weights,
570        threads=threads,
571    )

Run RPCMCI. regimes is required (length = series length); no silent half-split.

Call two_regime_half_split(len(series)) for an explicit two-regime mid-point split.

def discover_structure_mcmc( names: typing.Any | None = None, columns: Optional[Sequence[NDArray[numpy.float64]]] = None, *, data: typing.Any | None = None, n_chains: int = 4, n_warmup: int = 500, n_draws: int = 1000, thin: int = 1, seed: int = 1, threads: int = 1) -> GraphPosterior:
614def discover_structure_mcmc(
615    names: Any | None = None,
616    columns: Sequence[NDArray[np.float64]] | None = None,
617    *,
618    data: Any | None = None,
619    n_chains: int = 4,
620    n_warmup: int = 500,
621    n_draws: int = 1000,
622    thin: int = 1,
623    seed: int = 1,
624    threads: int = 1,
625) -> GraphPosterior:
626    n, cols = _coerce_tabular(names, columns, data=data)
627    return _discover_structure_mcmc(
628        n,
629        cols,
630        n_chains=n_chains,
631        n_warmup=n_warmup,
632        n_draws=n_draws,
633        thin=thin,
634        seed=seed,
635        threads=threads,
636    )
def two_regime_half_split(series_len):

Two-regime half-split assignment (opt-in helper for RPCMCI; not applied by default).

def encode_model_bundle(variable_names, edges, mechanisms):

Encode a minimal SCM model bundle (schema names + edges + mechanism slots).

mechanisms entries are (kind, constant|intercept, coeffs|None, sigma|None) with kind in {vacant, constant, linear_gaussian}.

def encode_posterior_artifact(artifact):

Re-encode a decoded [PosteriorArtifact] to container bytes (round-trip).

def evaluate_decision(actions, outcomes, utility):

Evaluate a decision problem under a Python utility callback .

utility(actions, outcomes) -> flat float64 ndarray of length len(actions) * len(outcomes).

def event( data: Union[Mapping[str, Any], Any], event_times_ns: Union[Sequence[int], NDArray[numpy.int64]], *, align_interval_ns: int) -> EventFrame:
48def event(
49    data: Mapping[str, Any] | Any,
50    event_times_ns: Sequence[int] | NDArray[np.int64],
51    *,
52    align_interval_ns: int,
53) -> EventFrame:
54    """Build an [`EventFrame`] for ``analyze`` (duration-bin align → temporal path)."""
55    if align_interval_ns <= 0:
56        raise ValueError("align_interval_ns must be > 0")
57    names, columns = as_columns(data)
58    times = np.asarray(event_times_ns, dtype=np.int64)
59    if times.ndim != 1:
60        raise ValueError(f"event_times_ns must be 1-d, got shape {times.shape}")
61    if len(times) != len(columns[0]):
62        raise ValueError(
63            f"event_times_ns length {len(times)} != column length {len(columns[0])}"
64        )
65    return EventFrame(
66        names=names,
67        columns=columns,
68        event_times_ns=times,
69        align_interval_ns=int(align_interval_ns),
70    )

Build an [EventFrame] for analyze (duration-bin align → temporal path).

@dataclass(frozen=True)
class EventFrame:
16@dataclass(frozen=True)
17class EventFrame:
18    """Irregular event marks + timestamps; aligned via ``align_interval_ns`` before temporal algos."""
19
20    names: list[str]
21    columns: list[NDArray[np.float64]]
22    event_times_ns: NDArray[np.int64]
23    align_interval_ns: int

Irregular event marks + timestamps; aligned via align_interval_ns before temporal algos.

EventFrame( names: list[str], columns: list[NDArray[numpy.float64]], event_times_ns: NDArray[numpy.int64], align_interval_ns: int)
names: list[str]
columns: list[NDArray[numpy.float64]]
event_times_ns: NDArray[numpy.int64]
align_interval_ns: int
def fit_gcm_discovered( data: Any, *, discovery: PC | GES | LiNGAM | NOTEARS, seed: int = 1, threads: int = 1):
56def fit_gcm_discovered(
57    data: Any,
58    *,
59    discovery: PC | GES | LiNGAM | NOTEARS,
60    seed: int = 1,
61    threads: int = 1,
62):
63    """Discover structure, coerce to a DAG, then ``fit_gcm``.
64
65    Returns ``(fitted_gcm, graph_edges)``. Incomplete CPDAG/PAG marks raise
66    ``ValueError`` (orientations are never invented). Structure provenance is
67    the caller-supplied ``discovery`` algorithm — attribution does not discover.
68    """
69    result, _algo = _run_static_discovery(data, discovery, seed=seed, threads=threads)
70    dag = discovery_to_dag(result)
71    names, columns = as_columns(data)
72    edges = list(dag.edges())
73    fitted = fit_gcm(names, columns, edges, threads=threads)
74    return fitted, edges

Discover structure, coerce to a DAG, then fit_gcm.

Returns (fitted_gcm, graph_edges). Incomplete CPDAG/PAG marks raise ValueError (orientations are never invented). Structure provenance is the caller-supplied discovery algorithm — attribution does not discover.

def load_float64_arrow_c_columns(names, columns):

Load float64 columns from Arrow C Data Interface exporters (PyArrow / __arrow_c_array__).

Prefers zero-copy borrow of contiguous float64 value buffers.

def load_float64_columns(names, columns):

Conversion probe: NumPy → Arrow → library-owned tabular storage.

Shares the same ingestion path as analyze* / discover_*. The loaded table is not retained across the FFI boundary; call analysis APIs with the original NumPy columns.

def mechanism_change_detection( names, columns, edges, baseline_start, baseline_end, comparison_start, comparison_end, *, seed=0, threads=1):

Detect mechanism changes across two row ranges.

def mediation_effects_summary(names, columns, treatment, mediator, outcome, *, seed=1, threads=1):

Mediation effect surface summary (total / direct / mediated).

def multi_env( envs: Sequence[Union[Mapping[str, Any], Any]]) -> MultiEnvFrame:
 97def multi_env(
 98    envs: Sequence[Mapping[str, Any] | Any],
 99) -> MultiEnvFrame:
100    """Build a [`MultiEnvFrame`] (sequence of environment frames for J-PCMCI+)."""
101    names, env_columns = as_multi_env_columns(envs)
102    return MultiEnvFrame(names=names, env_columns=env_columns)

Build a [MultiEnvFrame] (sequence of environment frames for J-PCMCI+).

@dataclass(frozen=True)
class MultiEnvFrame:
40@dataclass(frozen=True)
41class MultiEnvFrame:
42    """Multi-environment series (J-PCMCI+ discovery)."""
43
44    names: list[str]
45    env_columns: list[list[NDArray[np.float64]]]

Multi-environment series (J-PCMCI+ discovery).

MultiEnvFrame(names: list[str], env_columns: list[list[NDArray[numpy.float64]]])
names: list[str]
env_columns: list[list[NDArray[numpy.float64]]]
def panel( units: Union[Sequence[Union[Mapping[str, Any], Any]], Mapping[Any, Union[Mapping[str, Any], Any]]]) -> PanelFrame:
73def panel(
74    units: Sequence[Mapping[str, Any] | Any] | Mapping[Any, Mapping[str, Any] | Any],
75) -> PanelFrame:
76    """Build a [`PanelFrame`] from a sequence of unit frames or ``{unit_id: frame}``."""
77    if isinstance(units, Mapping):
78        ids = [int(k) for k in units.keys()]
79        frames = list(units.values())
80    else:
81        frames = list(units)
82        ids = list(range(len(frames)))
83    if not frames:
84        raise ValueError("panel needs ≥1 unit")
85    names, first = as_columns(frames[0])
86    unit_columns = [first]
87    for i, frame in enumerate(frames[1:], start=1):
88        n, cols = as_columns(frame)
89        if n != names:
90            raise ValueError(
91                f"unit {i} column names {n!r} do not match unit 0 {names!r}"
92            )
93        unit_columns.append(cols)
94    return PanelFrame(names=names, unit_columns=unit_columns, unit_ids=ids)

Build a [PanelFrame] from a sequence of unit frames or {unit_id: frame}.

@dataclass(frozen=True)
class PanelFrame:
26@dataclass(frozen=True)
27class PanelFrame:
28    """Multi-unit time series sharing one schema.
29
30    Discovery: ``JPCMCIPlus`` (multi-env context) or pooled-units ``PCMCI`` /
31    ``PCMCIPlus`` / ``LPCMCI``. Estimation uses stacked cluster-HAC SE
32    (frequentist) or ``BayesianTemporalGcomp`` when ``inference=Bayesian``.
33    """
34
35    names: list[str]
36    unit_columns: list[list[NDArray[np.float64]]]
37    unit_ids: list[int]

Multi-unit time series sharing one schema.

Discovery: JPCMCIPlus (multi-env context) or pooled-units PCMCI / PCMCIPlus / LPCMCI. Estimation uses stacked cluster-HAC SE (frequentist) or BayesianTemporalGcomp when inference=Bayesian.

PanelFrame( names: list[str], unit_columns: list[list[NDArray[numpy.float64]]], unit_ids: list[int])
names: list[str]
unit_columns: list[list[NDArray[numpy.float64]]]
unit_ids: list[int]
def predict_intervened_summary(names, columns, target, parent, *, parent_lag=1, level=1.0):

Intervene+predict summary (mean predicted outcome under do(parent=level)).

def rank_designs( graph_weights, identified, graph_keys, candidates, objective=None, *, query_id=None, model_ids=None, decision_id=None, query_id_unlock=None, env_id_unlock=None, identified_under_intervention=None, graph_features=None, effect_width=None, model_loglik=None, max_cost=None, max_sample_budget=None, min_batches=2, max_batches=64, batch_size=8, rank_uncertainty_threshold=0.05, seed=0, threads=1):

Rank candidate designs under a full [DesignRanker] objective / context.

def rank_root_causes(attribution, *, seed=0, threads=1):

Rank root causes from a [ChangeAttributionResult].

def sample_do( names, columns, edges, treatment, do_value, n_draws, *, seed=0, threads=1, mechanism_wrappers=None):

Fit GCM and return interventional column means + draws under hard do(treatment=value).

mechanism_wrappers maps variable name → object with sample_noise(n) / evaluate(parents, noise) ( slow path).

def sample_interventional_distribution( names, columns, edges, treatment, do_value, n_draws, outcome=None, *, seed=0, threads=1):

Sample an interventional distribution via [InterventionalDistributionQuery].

Same return shape as [gcm_sample_do]; builds the typed query then samples.

def target_all() -> dict[str, str]:
28def target_all() -> dict[str, str]:
29    return {"kind": "all"}
def target_custom_distribution(distribution_id: int) -> dict[str, object]:
48def target_custom_distribution(distribution_id: int) -> dict[str, object]:
49    return {"kind": "custom_distribution", "id": int(distribution_id)}
def target_named(name: str) -> dict[str, str]:
40def target_named(name: str) -> dict[str, str]:
41    return {"kind": "named", "name": str(name)}
def target_rows(rows: Sequence[int]) -> dict[str, object]:
44def target_rows(rows: Sequence[int]) -> dict[str, object]:
45    return {"kind": "rows", "rows": [int(r) for r in rows]}
def target_treated() -> dict[str, str]:
32def target_treated() -> dict[str, str]:
33    return {"kind": "treated"}
def target_untreated() -> dict[str, str]:
36def target_untreated() -> dict[str, str]:
37    return {"kind": "untreated"}
def attribute_paths_discovered( data: Any, *, discovery: PC | GES | LiNGAM | NOTEARS, sources: Sequence[str], outcome: str, max_paths: int = 64, max_len: int = 16, seed: int = 1, threads: int = 1):
 77def attribute_paths_discovered(
 78    data: Any,
 79    *,
 80    discovery: PC | GES | LiNGAM | NOTEARS,
 81    sources: Sequence[str],
 82    outcome: str,
 83    max_paths: int = 64,
 84    max_len: int = 16,
 85    seed: int = 1,
 86    threads: int = 1,
 87):
 88    """``fit_gcm_discovered`` then ``attribute_paths``. Returns ``(result, graph_edges)``."""
 89    fitted, edges = fit_gcm_discovered(
 90        data, discovery=discovery, seed=seed, threads=threads
 91    )
 92    _ = fitted
 93    names, columns = as_columns(data)
 94    result = attribute_paths(
 95        names,
 96        columns,
 97        edges,
 98        list(sources),
 99        outcome,
100        max_paths=max_paths,
101        max_len=max_len,
102        seed=seed,
103        threads=threads,
104    )
105    return result, edges

fit_gcm_discovered then attribute_paths. Returns (result, graph_edges).

def anomaly_attribution_discovered( data: Any, *, discovery: PC | GES | LiNGAM | NOTEARS, outcomes: Sequence[str], max_units: int = 0, seed: int = 1, threads: int = 1):
108def anomaly_attribution_discovered(
109    data: Any,
110    *,
111    discovery: PC | GES | LiNGAM | NOTEARS,
112    outcomes: Sequence[str],
113    max_units: int = 0,
114    seed: int = 1,
115    threads: int = 1,
116):
117    """``fit_gcm_discovered`` then ``anomaly_attribution``. Returns ``(result, graph_edges)``."""
118    fitted, edges = fit_gcm_discovered(
119        data, discovery=discovery, seed=seed, threads=threads
120    )
121    _ = fitted
122    names, columns = as_columns(data)
123    result = anomaly_attribution(
124        names, columns, edges, list(outcomes), max_units=max_units
125    )
126    return result, edges

fit_gcm_discovered then anomaly_attribution. Returns (result, graph_edges).

def attribute_distribution_change_discovered( data: Any, *, discovery: PC | GES | LiNGAM | NOTEARS, outcome: str, baseline_start: int, baseline_end: int, comparison_start: int, comparison_end: int, n_samples: int = 500, seed: int = 1, threads: int = 1):
129def attribute_distribution_change_discovered(
130    data: Any,
131    *,
132    discovery: PC | GES | LiNGAM | NOTEARS,
133    outcome: str,
134    baseline_start: int,
135    baseline_end: int,
136    comparison_start: int,
137    comparison_end: int,
138    n_samples: int = 500,
139    seed: int = 1,
140    threads: int = 1,
141):
142    """Compose discover → DAG → ``attribute_distribution_change``."""
143    fitted, edges = fit_gcm_discovered(
144        data, discovery=discovery, seed=seed, threads=threads
145    )
146    _ = fitted
147    names, columns = as_columns(data)
148    result = attribute_distribution_change(
149        names,
150        columns,
151        edges,
152        outcome,
153        baseline_start,
154        baseline_end,
155        comparison_start,
156        comparison_end,
157        n_samples=n_samples,
158        seed=seed,
159        threads=threads,
160    )
161    return result, edges

Compose discover → DAG → attribute_distribution_change.

def validate_environment_holdout( data: Sequence[Union[Mapping[str, Any], Any]], *, max_lag: int = 1, alpha: float = 0.05, fdr: bool = False, ci: str = 'parcorr', n_discovery: int = 1, seed: int = 1, threads: int = 1) -> dict[str, typing.Any]:
202def validate_environment_holdout(
203    data: Sequence[Mapping[str, Any] | Any],
204    *,
205    max_lag: int = 1,
206    alpha: float = 0.05,
207    fdr: bool = False,
208    ci: str = "parcorr",
209    n_discovery: int = 1,
210    seed: int = 1,
211    threads: int = 1,
212) -> dict[str, Any]:
213    names, env_columns = as_multi_env_columns(data)
214    return _validate_environment_holdout(
215        names,
216        env_columns,
217        max_lag=max_lag,
218        alpha=alpha,
219        fdr=fdr,
220        ci=ci,
221        n_discovery=n_discovery,
222        seed=seed,
223        threads=threads,
224    )
def validate_pcmci_alpha_sensitivity( data: Union[Mapping[str, Any], Any], alphas: Sequence[float], *, max_lag: int = 1, fdr: bool = False, ci: str = 'parcorr', seed: int = 1, threads: int = 1) -> dict[str, typing.Any]:
 81def validate_pcmci_alpha_sensitivity(
 82    data: Mapping[str, Any] | Any,
 83    alphas: Sequence[float],
 84    *,
 85    max_lag: int = 1,
 86    fdr: bool = False,
 87    ci: str = "parcorr",
 88    seed: int = 1,
 89    threads: int = 1,
 90) -> dict[str, Any]:
 91    names, columns = as_columns(data)
 92    return _validate_pcmci_alpha_sensitivity(
 93        names,
 94        columns,
 95        list(alphas),
 96        max_lag=max_lag,
 97        fdr=fdr,
 98        ci=ci,
 99        seed=seed,
100        threads=threads,
101    )
def validate_pcmci_block_bootstrap( data: Union[Mapping[str, Any], Any], *, max_lag: int = 1, alpha: float = 0.05, fdr: bool = False, ci: str = 'parcorr', replicates: int = 20, block_size: int = 20, seed: int = 1, threads: int = 1) -> dict[str, typing.Any]:
27def validate_pcmci_block_bootstrap(
28    data: Mapping[str, Any] | Any,
29    *,
30    max_lag: int = 1,
31    alpha: float = 0.05,
32    fdr: bool = False,
33    ci: str = "parcorr",
34    replicates: int = 20,
35    block_size: int = 20,
36    seed: int = 1,
37    threads: int = 1,
38) -> dict[str, Any]:
39    names, columns = as_columns(data)
40    return _validate_pcmci_block_bootstrap(
41        names,
42        columns,
43        max_lag=max_lag,
44        alpha=alpha,
45        fdr=fdr,
46        ci=ci,
47        replicates=replicates,
48        block_size=block_size,
49        seed=seed,
50        threads=threads,
51    )
def validate_pcmci_ci_sensitivity( data: Union[Mapping[str, Any], Any], ci_names: Sequence[str], *, max_lag: int = 1, alpha: float = 0.05, fdr: bool = False, seed: int = 1, threads: int = 1) -> dict[str, typing.Any]:
127def validate_pcmci_ci_sensitivity(
128    data: Mapping[str, Any] | Any,
129    ci_names: Sequence[str],
130    *,
131    max_lag: int = 1,
132    alpha: float = 0.05,
133    fdr: bool = False,
134    seed: int = 1,
135    threads: int = 1,
136) -> dict[str, Any]:
137    names, columns = as_columns(data)
138    return _validate_pcmci_ci_sensitivity(
139        names,
140        columns,
141        list(ci_names),
142        max_lag=max_lag,
143        alpha=alpha,
144        fdr=fdr,
145        seed=seed,
146        threads=threads,
147    )
def validate_pcmci_false_positive( data: Union[Mapping[str, Any], Any], *, max_lag: int = 1, alpha: float = 0.05, fdr: bool = False, ci: str = 'parcorr', transform: str = 'permute', replicates: int = 20, seed: int = 1, threads: int = 1) -> dict[str, typing.Any]:
54def validate_pcmci_false_positive(
55    data: Mapping[str, Any] | Any,
56    *,
57    max_lag: int = 1,
58    alpha: float = 0.05,
59    fdr: bool = False,
60    ci: str = "parcorr",
61    transform: str = "permute",
62    replicates: int = 20,
63    seed: int = 1,
64    threads: int = 1,
65) -> dict[str, Any]:
66    names, columns = as_columns(data)
67    return _validate_pcmci_false_positive(
68        names,
69        columns,
70        max_lag=max_lag,
71        alpha=alpha,
72        fdr=fdr,
73        ci=ci,
74        transform=transform,
75        replicates=replicates,
76        seed=seed,
77        threads=threads,
78    )
def validate_pcmci_lag_sensitivity( data: Union[Mapping[str, Any], Any], max_lags: Sequence[int], *, alpha: float = 0.05, fdr: bool = False, ci: str = 'parcorr', seed: int = 1, threads: int = 1) -> dict[str, typing.Any]:
104def validate_pcmci_lag_sensitivity(
105    data: Mapping[str, Any] | Any,
106    max_lags: Sequence[int],
107    *,
108    alpha: float = 0.05,
109    fdr: bool = False,
110    ci: str = "parcorr",
111    seed: int = 1,
112    threads: int = 1,
113) -> dict[str, Any]:
114    names, columns = as_columns(data)
115    return _validate_pcmci_lag_sensitivity(
116        names,
117        columns,
118        [int(m) for m in max_lags],
119        alpha=alpha,
120        fdr=fdr,
121        ci=ci,
122        seed=seed,
123        threads=threads,
124    )
def validate_pcmci_plus_orientation( data: Union[Mapping[str, Any], Any], *, max_lag: int = 1, alpha: float = 0.05, fdr: bool = False, ci: str = 'parcorr', replicates: int = 20, block_size: int = 20, seed: int = 1, threads: int = 1) -> dict[str, typing.Any]:
150def validate_pcmci_plus_orientation(
151    data: Mapping[str, Any] | Any,
152    *,
153    max_lag: int = 1,
154    alpha: float = 0.05,
155    fdr: bool = False,
156    ci: str = "parcorr",
157    replicates: int = 20,
158    block_size: int = 20,
159    seed: int = 1,
160    threads: int = 1,
161) -> dict[str, Any]:
162    names, columns = as_columns(data)
163    return _validate_pcmci_plus_orientation(
164        names,
165        columns,
166        max_lag=max_lag,
167        alpha=alpha,
168        fdr=fdr,
169        ci=ci,
170        replicates=replicates,
171        block_size=block_size,
172        seed=seed,
173        threads=threads,
174    )
def validate_regime_stability( data: Union[Mapping[str, Any], Any], regimes: Sequence[int], *, max_lag: int = 1, alpha: float = 0.05, fdr: bool = False, ci: str = 'parcorr', replicates: int = 10, block_size: int = 20, seed: int = 1, threads: int = 1) -> dict[str, typing.Any]:
227def validate_regime_stability(
228    data: Mapping[str, Any] | Any,
229    regimes: Sequence[int],
230    *,
231    max_lag: int = 1,
232    alpha: float = 0.05,
233    fdr: bool = False,
234    ci: str = "parcorr",
235    replicates: int = 10,
236    block_size: int = 20,
237    seed: int = 1,
238    threads: int = 1,
239) -> dict[str, Any]:
240    names, columns = as_columns(data)
241    return _validate_regime_stability(
242        names,
243        columns,
244        [int(r) for r in regimes],
245        max_lag=max_lag,
246        alpha=alpha,
247        fdr=fdr,
248        ci=ci,
249        replicates=replicates,
250        block_size=block_size,
251        seed=seed,
252        threads=threads,
253    )
def validate_synthetic_null_calibration( *, max_lag: int = 1, alpha: float = 0.05, fdr: bool = False, ci: str = 'parcorr', n_sim: int = 20, n_obs: int = 100, n_vars: int = 3, seed: int = 1, threads: int = 1) -> dict[str, typing.Any]:
177def validate_synthetic_null_calibration(
178    *,
179    max_lag: int = 1,
180    alpha: float = 0.05,
181    fdr: bool = False,
182    ci: str = "parcorr",
183    n_sim: int = 20,
184    n_obs: int = 100,
185    n_vars: int = 3,
186    seed: int = 1,
187    threads: int = 1,
188) -> dict[str, Any]:
189    return _validate_synthetic_null_calibration(
190        max_lag=max_lag,
191        alpha=alpha,
192        fdr=fdr,
193        ci=ci,
194        n_sim=n_sim,
195        n_obs=n_obs,
196        n_vars=n_vars,
197        seed=seed,
198        threads=threads,
199    )
__version__: str = '0.2.0'