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"
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.
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.
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.
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.
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.
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.
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.
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).
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().
Named ADMG (directed + bidirected).
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.
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).
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.
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.
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).
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.
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.
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.
Named static CPDAG.
Named static DAG.
Latent-project onto observed variable names; returns an Admg.
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).
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).
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).
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).
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).
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).
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).
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).
Named static PAG.
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).
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.
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.
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).
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.
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.
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.
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.
Named temporal DAG over lagged variables.
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).
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.
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.
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().
Fit a linear-Gaussian GCM; return a reusable [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.
Interventional ancestral sample under hard do values.
Unit-level ITE under hard interventions on treatment.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Common base class for all non-exit exceptions.
Cooperative cancellation token shared with a running analysis.
Common base class for all non-exit exceptions.
Anomaly scores for one outcome.
Result of the conversion probe (same Arrow→tabular path as analyze/discover).
Change-attribution summary (components and optional path breakdown).
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.
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
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"
Named contribution (node or path label).
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).
Decision evaluation under a utility callback.
Full design ranking result.
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.
One discovered lagged link for Python.
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).
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.
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"
Feature relevance under interventions.
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"
Result of a GCM counterfactual ITE (single boundary crossing).
Interventional samples under hard do (means + full draws).
One marked edge from an oriented temporal CPDAG/PAG.
Columnar graph posterior returned by Bayesian discovery engines.
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"
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"
Mechanism-change detection for one node.
Mediation effects summary.
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"
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"
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"
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"
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"
Coarse-grained PCMCI discovery result (single boundary crossing).
Field set is the stable Rust↔Python temporal discovery schema for .
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.
Decoded posterior artifact for Python consumers .
Prediction summary under intervention.
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).
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.
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.
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.
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.
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.
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=...).
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.
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.
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.
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
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 )
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=...).
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
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.
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.
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.
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.
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).
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).
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"
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 summary (typed regimes, no single-graph collapse).
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"
Named temporal CPDAG over lagged variables.
Named temporal PAG over lagged variables.
Incremental causal analysis state (versioned events, no auto-rerun).
Number of registered queries whose cached results are currently stale.
Append a tabular batch and retain its float64 columns.
Returns the new state version.
Replace all retained batches; optionally load one new batch.
Returns the new state version.
Add opaque graph evidence; returns new version.
Add a graph constraint; returns new version.
Remove a graph constraint by id; returns new version.
Update / insert a named assumption (default provenance: stated / identification / assumed).
Register a binary average-effect query; returns (version, query_id).
Record an opaque intervention; returns new version.
Mark queries fresh at fingerprints: list of (query_id, fingerprint, bytes).
Ensure an OLS sufficient-stat slot exists with ncols predictors.
Initialize a particle filter under key.
Anomaly scores for listed outcomes.
Fit GCM and attribute distribution change between two row ranges via Shapley.
Robust distribution-change attribution between two row ranges.
Feature relevance scores for parents of outcome.
Path-specific contribution via [PathSpecificEffectQuery] / path_decompose.
Path decomposition (all paths from sources to outcome).
Structure-change attribution between two edge lists (parent-set Shapley).
Unit-level change attribution.
Fit a linear-Gaussian GCM and return mean ITE under hard interventions.
Crosses the Python boundary once: NumPy columns + edges in, arrays out.
Parse DOT digraph text; return (node_count, edges).
Parse GML digraph text; return (node_count, edges).
Parse JSON DAG document; return (node_count, edges, variable_names|None).
Parse NetworkX adjacency JSON; return (node_count, edges).
Parse NetworkX node-link JSON; return (node_count, edges).
Emit DOT for a numeric DAG given node_count and edges.
Emit GML for a numeric DAG.
Emit JSON for a numeric DAG.
Emit NetworkX adjacency JSON for a numeric DAG.
Emit NetworkX node-link JSON for a numeric DAG.
Decode a model bundle; return (variable_names, edges, n_mechanisms).
Decode a serialized posterior artifact into summaries + column-major draws.
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 )
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.
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.
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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.
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 )
Two-regime half-split assignment (opt-in helper for RPCMCI; not applied by default).
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}.
Re-encode a decoded [PosteriorArtifact] to container bytes (round-trip).
Evaluate a decision problem under a Python utility callback .
utility(actions, outcomes) -> flat float64 ndarray of length len(actions) * len(outcomes).
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).
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.
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.
Load float64 columns from Arrow C Data Interface exporters (PyArrow / __arrow_c_array__).
Prefers zero-copy borrow of contiguous float64 value buffers.
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.
Detect mechanism changes across two row ranges.
Mediation effect surface summary (total / direct / mediated).
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+).
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).
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}.
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.
Intervene+predict summary (mean predicted outcome under do(parent=level)).
Rank candidate designs under a full [DesignRanker] objective / context.
Rank root causes from a [ChangeAttributionResult].
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).
Sample an interventional distribution via [InterventionalDistributionQuery].
Same return shape as [gcm_sample_do]; builds the typed query then samples.
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).
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).
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.
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )