antecedent.estimation
Estimation stubs.
1"""High-level estimation entry points.""" 2 3from __future__ import annotations 4 5from dataclasses import dataclass 6from typing import Any, Literal, Mapping, Sequence 7 8from ._data import as_columns, as_multi_env_columns, try_as_arrow_c_columns 9from ._native import ( 10 AnalysisResult as TemporalAnalysisResult, 11 AteAnalysisResult, 12 CausalUnsupportedError, 13 PreparedAnalysis as _NativePreparedAnalysis, 14 analyze as _analyze_temporal, 15 analyze_ate as _analyze_ate, 16 analyze_ate_admg as _analyze_ate_admg, 17 analyze_ate_arrow_c as _analyze_ate_arrow_c, 18 analyze_ate_cpdag as _analyze_ate_cpdag, 19 analyze_ate_discover as _analyze_ate_discover, 20 analyze_ate_many as _analyze_ate_many, 21 analyze_ate_pag as _analyze_ate_pag, 22 analyze_conditional as _analyze_conditional, 23 analyze_distribution as _analyze_distribution, 24 analyze_events as _analyze_events, 25 analyze_mediation as _analyze_mediation, 26 analyze_panel as _analyze_panel, 27 analyze_panel_discover as _analyze_panel_discover, 28 analyze_path_specific as _analyze_path_specific, 29 analyze_temporal_discover as _analyze_temporal_discover, 30 analyze_temporal_mediation as _analyze_temporal_mediation, 31 analyze_temporal_pag as _analyze_temporal_pag, 32 identify_ate as _identify_ate, 33) 34from .data import EventFrame, MultiEnvFrame, PanelFrame 35from .discovery import ( 36 FCI, 37 GES, 38 JPCMCIPlus, 39 LPCMCI, 40 LiNGAM, 41 NOTEARS, 42 PC, 43 PCMCI, 44 PCMCIPlus, 45 RFCI, 46 RPCMCI, 47 CiScreenedPosterior, 48 DbnPosterior, 49 ExactDagPosterior, 50 OrderMcmc, 51 StructureMcmc, 52 cpdag_oriented_edges, 53 discovery_to_dag, 54 graph_posterior_map_edges, 55) 56from .graph import Admg, Cpdag, Dag, Pag, TemporalCpdag, TemporalDag, TemporalPag 57from .inference import Bayesian, Frequentist 58from .query import ( 59 AverageEffect, 60 ConditionalEffect, 61 Counterfactual, 62 InterventionalDistribution, 63 MediationEffect, 64 PathSpecificEffect, 65 PulseEffect, 66 SustainedEffect, 67 TemporalMediationEffect, 68) 69from .ids import Estimator, Identifier, Latency, Refute 70 71# Preferred name for the native temporal DTO. 72NativeAnalysisResult = TemporalAnalysisResult 73 74 75@dataclass(frozen=True) 76class IdentificationView: 77 status: str 78 method: str 79 adjustment_set: list[str] 80 assumption_count: int 81 derivation_step_count: int 82 83 84@dataclass(frozen=True) 85class MediationView: 86 total: float | None 87 direct: float | None 88 mediated: float | None 89 90 91@dataclass(frozen=True) 92class EstimateView: 93 ate: float 94 se_analytic: float 95 se_bootstrap: float | None 96 estimator_id: str 97 method: str 98 overlap_ess: float | None = None 99 overlap_propensity_min: float | None = None 100 mediation: MediationView | None = None 101 102 103@dataclass(frozen=True) 104class ConflictSummaryView: 105 """Applied external-prior alphas after conflict shrink.""" 106 107 source_ids: list[str] 108 alphas_requested: list[float] 109 alphas_applied: list[float] 110 111 112@dataclass(frozen=True) 113class PosteriorView: 114 effect_mean: float | None 115 effect_sd: float | None 116 q025: float | None 117 q975: float | None 118 n_draws: int | None 119 p_below_zero: float | None 120 backend: str | None 121 artifact: bytes | list[int] | None = None 122 unidentified_mass: float | None = None 123 envelope: EffectEnvelope | None = None 124 conflict: ConflictSummaryView | None = None 125 126 127@dataclass(frozen=True) 128class EffectEnvelope: 129 """Mixture effect posterior over weighted graphs (PAG / graph-posterior path).""" 130 131 effect_mean: float | None 132 effect_sd: float | None 133 q025: float | None 134 q975: float | None 135 unidentified_mass: float 136 n_draws: int | None 137 backend: str | None = None 138 139 140@dataclass(frozen=True) 141class PredictiveCheckReport: 142 """Prior or posterior predictive check summary.""" 143 144 kind: str 145 observed: float 146 predictive_mean: float 147 predictive_sd: float 148 p_value: float 149 n_sims: int 150 151 152@dataclass(frozen=True) 153class PriorSensitivityReport: 154 """Prior sensitivity grid (Bayesian + ``refute="full"``). 155 156 Isotropic mode fills ``scales``; external prior-bank mode fills ``alphas`` 157 (multipliers on post-conflict applied α). Exactly one mode is active. 158 """ 159 160 scales: list[float] 161 effect_means: list[float] 162 effect_sds: list[float] 163 alphas: list[float] | None = None 164 165 166@dataclass(frozen=True) 167class ValidationView: 168 passed: bool 169 ran: bool 170 count: int 171 prior_predictive: PredictiveCheckReport | None = None 172 posterior_predictive: PredictiveCheckReport | None = None 173 prior_sensitivity: PriorSensitivityReport | None = None 174 175 176@dataclass(frozen=True) 177class PerformanceView: 178 plan_id: str | None = None 179 modality: str | None = None 180 peak_memory_bytes: int | None = None 181 latency_mode: str | None = None 182 wall_time_ns: int | None = None 183 bootstrap_replicates_requested: int | None = None 184 bootstrap_replicates_ok: int | None = None 185 n_draws: int | None = None 186 cancelled: bool = False 187 early_stopped: bool = False 188 stage_timings: dict[str, int] | None = None 189 190 191@dataclass(frozen=True) 192class PlanView: 193 """Logical-plan summary (semantics; inspect before/after estimate).""" 194 195 plan_id: str 196 modality: str | None = None 197 discovery_algorithm: str | None = None 198 graph_review_required: bool = False 199 identifier: str | None = None 200 estimator: str | None = None 201 validation_suite: str | None = None 202 203 204@dataclass(frozen=True) 205class PhysicalPlanView: 206 """Physical-plan highlights from prepare (layouts / threads / kernels).""" 207 208 plan_id: str 209 estimated_peak_memory_bytes: int | None = None 210 workspace_bytes: int | None = None 211 batch_size: int | None = None 212 worker_threads: int = 0 213 expected_python_crossings: int = 0 214 deterministic_reductions: bool = True 215 kernels: str | None = None 216 217 218@dataclass(frozen=True) 219class AnalysisResult: 220 """Nested analysis result matching the Rust facade sections.""" 221 222 identification: IdentificationView 223 estimate: EstimateView 224 posterior: PosteriorView | None 225 validation: ValidationView 226 performance: PerformanceView 227 diagnostics: list[str] 228 provenance: dict[str, Any] 229 mediation: MediationView | None = None 230 plan: PlanView | None = None 231 _raw: Any = None 232 _prepared: Any = None 233 234 @property 235 def effect(self) -> float: 236 """Primary scalar effect (mediation total when present, else estimate ATE/mean).""" 237 if self.mediation is not None and self.mediation.total is not None: 238 return float(self.mediation.total) 239 if self.estimate.mediation is not None and self.estimate.mediation.total is not None: 240 return float(self.estimate.mediation.total) 241 return self.estimate.ate 242 243 @property 244 def ate(self) -> float: 245 """Alias for :attr:`effect` (prefer ``effect`` for non-ATE queries).""" 246 return self.effect 247 248 def refresh( 249 self, 250 data: Mapping[str, Any] | Any, 251 *, 252 seed: int = 1, 253 threads: int = 1, 254 ) -> AnalysisResult: 255 """Re-estimate on new data via the retained prepared handle. 256 257 Only results from :meth:`PreparedAnalysis.estimate` / ``refresh`` support 258 this. One-shot :func:`analyze` results raise ``TypeError``. 259 """ 260 if self._prepared is None: 261 raise TypeError( 262 "AnalysisResult.refresh requires a result from PreparedAnalysis; " 263 "use PreparedAnalysis.prepare(...) then estimate/refresh" 264 ) 265 return self._prepared.estimate(data, seed=seed, threads=threads) 266 267 def refute( 268 self, 269 data: Mapping[str, Any] | Any, 270 suite: Refute | Literal["placebo", "full", "cheap"] | bool | str = "placebo", 271 *, 272 seed: int = 1, 273 threads: int = 1, 274 cancel: Any | None = None, 275 ) -> AnalysisResult: 276 """Second-click refute via the retained prepared handle.""" 277 if self._prepared is None: 278 raise TypeError( 279 "AnalysisResult.refute requires a result from PreparedAnalysis; " 280 "use PreparedAnalysis.prepare(...) then estimate" 281 ) 282 if isinstance(suite, Refute): 283 suite = str(suite) 284 return self._prepared.refute( 285 data, suite, seed=seed, threads=threads, cancel=cancel 286 ) 287 288 289def _plan_from_raw(raw: Any) -> PlanView: 290 return PlanView( 291 plan_id=str(getattr(raw, "plan_id", "") or ""), 292 modality=getattr(raw, "modality", None), 293 discovery_algorithm=getattr(raw, "discovery_algorithm", None), 294 graph_review_required=bool(getattr(raw, "graph_review_required", False)), 295 identifier=getattr(raw, "plan_identifier", None), 296 estimator=getattr(raw, "plan_estimator", None) 297 or (getattr(raw, "estimator_id", None) or None), 298 validation_suite=getattr(raw, "validation_suite", None), 299 ) 300 301 302def _wrap_ate(raw: AteAnalysisResult, prepared: Any | None = None) -> AnalysisResult: 303 def _conflict_from_raw(r: AteAnalysisResult) -> ConflictSummaryView | None: 304 ids = getattr(r, "conflict_source_ids", None) 305 if ids is None: 306 return None 307 return ConflictSummaryView( 308 source_ids=list(ids), 309 alphas_requested=list(r.conflict_alphas_requested), 310 alphas_applied=list(r.conflict_alphas_applied), 311 ) 312 313 posterior = None 314 if raw.posterior_n_draws is not None: 315 mass = getattr(raw, "posterior_unidentified_mass", None) 316 envelope = None 317 if mass is not None and float(mass) > 0.0: 318 envelope = EffectEnvelope( 319 effect_mean=raw.posterior_effect_mean, 320 effect_sd=raw.posterior_effect_sd, 321 q025=raw.posterior_q025, 322 q975=raw.posterior_q975, 323 unidentified_mass=float(mass), 324 n_draws=raw.posterior_n_draws, 325 backend=raw.posterior_backend, 326 ) 327 posterior = PosteriorView( 328 effect_mean=raw.posterior_effect_mean, 329 effect_sd=raw.posterior_effect_sd, 330 q025=raw.posterior_q025, 331 q975=raw.posterior_q975, 332 n_draws=raw.posterior_n_draws, 333 p_below_zero=raw.posterior_p_below_zero, 334 backend=raw.posterior_backend, 335 artifact=raw.posterior_artifact, 336 unidentified_mass=None if mass is None else float(mass), 337 envelope=envelope, 338 conflict=_conflict_from_raw(raw), 339 ) 340 prior_predictive = None 341 if getattr(raw, "prior_ppc_p_value", None) is not None: 342 prior_predictive = PredictiveCheckReport( 343 kind="prior_predictive", 344 observed=float(raw.prior_ppc_observed), 345 predictive_mean=float(raw.prior_ppc_predictive_mean), 346 predictive_sd=float(raw.prior_ppc_predictive_sd), 347 p_value=float(raw.prior_ppc_p_value), 348 n_sims=int(raw.prior_ppc_n_sims), 349 ) 350 posterior_predictive = None 351 if getattr(raw, "posterior_ppc_p_value", None) is not None: 352 posterior_predictive = PredictiveCheckReport( 353 kind="posterior_predictive", 354 observed=float(raw.posterior_ppc_observed), 355 predictive_mean=float(raw.posterior_ppc_predictive_mean), 356 predictive_sd=float(raw.posterior_ppc_predictive_sd), 357 p_value=float(raw.posterior_ppc_p_value), 358 n_sims=int(raw.posterior_ppc_n_sims), 359 ) 360 prior_sensitivity = None 361 means = getattr(raw, "prior_sensitivity_means", None) 362 if means is not None: 363 alphas_raw = getattr(raw, "prior_sensitivity_alphas", None) 364 scales_raw = getattr(raw, "prior_sensitivity_scales", None) 365 prior_sensitivity = PriorSensitivityReport( 366 scales=list(scales_raw or ()), 367 effect_means=list(means), 368 effect_sds=list(raw.prior_sensitivity_sds), 369 alphas=None if alphas_raw is None else list(alphas_raw), 370 ) 371 return AnalysisResult( 372 identification=IdentificationView( 373 status=raw.identification_status, 374 method=raw.method, 375 adjustment_set=list(raw.adjustment_set), 376 assumption_count=raw.assumption_count, 377 derivation_step_count=raw.derivation_step_count, 378 ), 379 estimate=EstimateView( 380 ate=raw.ate, 381 se_analytic=raw.se_analytic, 382 se_bootstrap=raw.se_bootstrap, 383 estimator_id=raw.estimator_id, 384 method=raw.method, 385 overlap_ess=raw.overlap_ess, 386 overlap_propensity_min=raw.overlap_propensity_min, 387 ), 388 posterior=posterior, 389 validation=ValidationView( 390 passed=raw.refutation_passed, 391 ran=raw.refutation_ran, 392 count=raw.refutation_count, 393 prior_predictive=prior_predictive, 394 posterior_predictive=posterior_predictive, 395 prior_sensitivity=prior_sensitivity, 396 ), 397 performance=PerformanceView( 398 plan_id=raw.plan_id, 399 modality=raw.modality, 400 peak_memory_bytes=raw.peak_memory_bytes, 401 latency_mode=getattr(raw, "latency_mode", None), 402 wall_time_ns=getattr(raw, "wall_time_ns", None), 403 bootstrap_replicates_requested=getattr( 404 raw, "bootstrap_replicates_requested", None 405 ), 406 bootstrap_replicates_ok=getattr(raw, "bootstrap_replicates_ok", None), 407 n_draws=getattr(raw, "n_draws_effort", None), 408 cancelled=bool(getattr(raw, "cancelled", False)), 409 early_stopped=bool(getattr(raw, "early_stopped", False)), 410 stage_timings={ 411 str(k): int(v) for k, v in (getattr(raw, "stage_timings", None) or []) 412 } 413 or None, 414 ), 415 diagnostics=list(raw.diagnostics), 416 provenance={"node_count": raw.provenance_node_count}, 417 plan=_plan_from_raw(raw), 418 _raw=raw, 419 _prepared=prepared, 420 ) 421 422 423def _resolve_latency_budget( 424 latency: Latency | Literal["interactive", "standard", "report"] | str | None, 425 bootstrap: int | None, 426 refute: bool | Refute | Literal["full", "placebo", "none", "cheap"] | str, 427) -> tuple[int, bool | Literal["full", "placebo", "none", "cheap"]]: 428 """Map latency tier to bootstrap/refute; explicit bootstrap wins when set.""" 429 if isinstance(latency, Latency): 430 latency = str(latency) 431 if isinstance(refute, Refute): 432 refute = str(refute) 433 if latency is None: 434 return (50 if bootstrap is None else bootstrap, refute) # type: ignore[return-value] 435 key = str(latency).strip().lower() 436 if key == "interactive": 437 mapped_boot, mapped_refute = 0, "cheap" 438 elif key == "standard": 439 mapped_boot, mapped_refute = 50, True 440 elif key == "report": 441 mapped_boot, mapped_refute = 200, "full" 442 else: 443 raise ValueError( 444 f"unknown latency={latency!r}; use interactive|standard|report" 445 ) 446 # refute default True means "use mode mapping" when latency is set unless 447 # the caller chose a non-default refute value. 448 out_refute: bool | Literal["full", "placebo", "none", "cheap"] 449 if refute is True: 450 out_refute = mapped_refute # type: ignore[assignment] 451 else: 452 out_refute = refute # type: ignore[assignment] 453 out_boot = mapped_boot if bootstrap is None else bootstrap 454 return out_boot, out_refute 455 456 457_STATIC_DISCOVERY = (PC, GES, LiNGAM, NOTEARS, FCI, RFCI) 458_GRAPH_POSTERIOR_DISCOVERY = ( 459 ExactDagPosterior, 460 OrderMcmc, 461 StructureMcmc, 462 CiScreenedPosterior, 463) 464_TEMPORAL_DISCOVERY = (PCMCI, PCMCIPlus, LPCMCI, JPCMCIPlus, RPCMCI) 465 466 467def _discovery_algorithm(discovery: Any) -> dict[str, Any]: 468 if isinstance(discovery, PCMCI): 469 return { 470 "algorithm": "pcmci", 471 "max_lag": discovery.max_lag, 472 "alpha": discovery.alpha, 473 "fdr": discovery.fdr, 474 "ci": discovery.ci, 475 } 476 if isinstance(discovery, PCMCIPlus): 477 return { 478 "algorithm": "pcmci_plus", 479 "max_lag": discovery.max_lag, 480 "alpha": discovery.alpha, 481 "fdr": discovery.fdr, 482 "ci": discovery.ci, 483 } 484 if isinstance(discovery, LPCMCI): 485 return { 486 "algorithm": "lpcmci", 487 "max_lag": discovery.max_lag, 488 "alpha": discovery.alpha, 489 "fdr": discovery.fdr, 490 "ci": discovery.ci, 491 } 492 if isinstance(discovery, JPCMCIPlus): 493 return { 494 "algorithm": "jpcmci_plus", 495 "max_lag": discovery.max_lag, 496 "alpha": discovery.alpha, 497 "fdr": discovery.fdr, 498 "ci": discovery.ci, 499 "context_names": list(discovery.context_names), 500 "include_space_dummy": discovery.include_space_dummy, 501 "include_time_dummy": discovery.include_time_dummy, 502 "space_dummy_ci": discovery.space_dummy_ci, 503 "time_dummy_encoding": discovery.time_dummy_encoding, 504 "time_dummy_ci": discovery.time_dummy_ci, 505 } 506 if isinstance(discovery, RPCMCI): 507 return { 508 "algorithm": "rpcmci", 509 "max_lag": discovery.max_lag, 510 "alpha": discovery.alpha, 511 "fdr": discovery.fdr, 512 "ci": discovery.ci, 513 } 514 if isinstance(discovery, PC): 515 return { 516 "algorithm": "pc", 517 "alpha": discovery.alpha, 518 "fdr": discovery.fdr, 519 "ci": discovery.ci, 520 "max_cond_size": discovery.max_cond_size, 521 } 522 if isinstance(discovery, GES): 523 return { 524 "algorithm": "ges", 525 "alpha": discovery.alpha, 526 "fdr": discovery.fdr, 527 "ci": discovery.ci, 528 "max_cond_size": discovery.max_cond_size, 529 } 530 if isinstance(discovery, LiNGAM): 531 return { 532 "algorithm": "lingam", 533 "prune_threshold": discovery.prune_threshold, 534 "max_cond_size": discovery.max_cond_size, 535 "alpha": 0.05, 536 "fdr": True, 537 "ci": "parcorr", 538 } 539 if isinstance(discovery, NOTEARS): 540 return { 541 "algorithm": "notears", 542 "lambda": discovery.l1, 543 "threshold": discovery.threshold, 544 "standardize": discovery.standardize, 545 "max_cond_size": discovery.max_cond_size, 546 "alpha": 0.05, 547 "fdr": True, 548 "ci": "parcorr", 549 } 550 if isinstance(discovery, FCI): 551 return { 552 "algorithm": "fci", 553 "alpha": discovery.alpha, 554 "fdr": discovery.fdr, 555 "ci": discovery.ci, 556 "max_cond_size": discovery.max_cond_size, 557 } 558 if isinstance(discovery, RFCI): 559 return { 560 "algorithm": "rfci", 561 "alpha": discovery.alpha, 562 "fdr": discovery.fdr, 563 "ci": discovery.ci, 564 "max_cond_size": discovery.max_cond_size, 565 } 566 if isinstance(discovery, ExactDagPosterior): 567 return {"algorithm": "exact_dag_posterior"} 568 if isinstance(discovery, OrderMcmc): 569 return { 570 "algorithm": "order_mcmc", 571 "n_chains": discovery.n_chains, 572 "n_warmup": discovery.n_warmup, 573 "mcmc_draws": discovery.n_draws, 574 "thin": discovery.thin, 575 "require_diagnostics_gate": discovery.require_diagnostics_gate, 576 } 577 if isinstance(discovery, StructureMcmc): 578 return { 579 "algorithm": "structure_mcmc", 580 "n_chains": discovery.n_chains, 581 "n_warmup": discovery.n_warmup, 582 "mcmc_draws": discovery.n_draws, 583 "thin": discovery.thin, 584 } 585 if isinstance(discovery, CiScreenedPosterior): 586 return { 587 "algorithm": "ci_screened_posterior", 588 "alpha": discovery.alpha, 589 "fdr": discovery.fdr, 590 "ci": discovery.ci, 591 "max_cond_size": discovery.max_cond_size, 592 "soft_weight": discovery.soft_weight, 593 "n_chains": discovery.n_chains, 594 "n_warmup": discovery.n_warmup, 595 "mcmc_draws": discovery.n_draws, 596 "thin": discovery.thin, 597 } 598 if isinstance(discovery, DbnPosterior): 599 return { 600 "algorithm": "dbn_posterior", 601 "max_lag": discovery.max_lag, 602 "force_mcmc": discovery.force_mcmc, 603 "n_chains": discovery.n_chains, 604 "n_warmup": discovery.n_warmup, 605 "mcmc_draws": discovery.n_draws, 606 } 607 raise TypeError(f"unsupported discovery config: {type(discovery)!r}") 608 609 610def _static_edges( 611 graph: Dag | Cpdag | Sequence[tuple[str, str]] | None, 612) -> list[tuple[str, str]]: 613 if graph is None: 614 raise ValueError("graph= is required") 615 if isinstance(graph, Dag): 616 return [(str(a), str(b)) for a, b in graph.edges()] 617 if isinstance(graph, Cpdag): 618 # PathSpecific / Interventional need a fully oriented DAG; incomplete 619 # CPDAGs fail closed with a clear undirected-count message. 620 return cpdag_oriented_edges(graph, require_oriented=True) 621 return [(str(a), str(b)) for a, b in graph] # type: ignore[misc] 622 623 624def _lagged_edges( 625 graph: TemporalDag | Sequence[tuple[str, int, str, int]] | None, 626) -> list[tuple[str, int, str, int]]: 627 if graph is None: 628 raise ValueError("graph= lagged edges are required") 629 if isinstance(graph, TemporalDag): 630 return [ 631 (str(a), int(la), str(b), int(lb)) for a, la, b, lb in graph.edges() 632 ] 633 return [(str(a), int(la), str(b), int(lb)) for a, la, b, lb in graph] # type: ignore[misc] 634 635 636def _refute_requested(refute: bool | str) -> bool: 637 """True when the caller asked for any non-empty refute suite.""" 638 if isinstance(refute, bool): 639 return refute 640 key = str(refute).strip().lower() 641 return key not in ("", "none", "off", "false", "0") 642 643 644def _reject_unsupported_temporal( 645 *, 646 inference: Frequentist | Bayesian | None, 647 refute: bool | str, 648 validators: Sequence[Any] | None, 649) -> None: 650 # Bayesian, refute, and validators are supported on series Pulse/Sustained. 651 _ = (inference, refute, validators) 652 return 653 654 655def _bayesian_inference_kwargs(inference: Bayesian) -> dict[str, Any]: 656 backend = str(inference.backend).strip().lower() 657 if backend == "laplace": 658 inference_s = "bayesian" 659 elif backend == "conjugate": 660 inference_s = "conjugate" 661 elif backend == "hmc": 662 inference_s = "hmc" 663 else: 664 raise ValueError( 665 f"unknown Bayesian backend {inference.backend!r}; " 666 "use laplace|conjugate|hmc" 667 ) 668 kw: dict[str, Any] = { 669 "inference": inference_s, 670 "n_draws": inference.n_draws, 671 "prior_scale": inference.prior_scale, 672 } 673 prior_from = inference.prior_from 674 if prior_from is not None: 675 # Local import avoids circular import with prior_bank ↔ estimation. 676 from .prior_bank import ComposedPrior 677 678 if isinstance(prior_from, ComposedPrior): 679 kw["composed_prior"] = prior_from.to_native_dict() 680 else: 681 kw["prior_artifact"] = bytes(prior_from) 682 if inference.mapping is not None: 683 kw["prior_mapping"] = inference.mapping.to_dict() 684 return kw 685 686 687def _temporal_inference_kwargs( 688 inference: Frequentist | Bayesian | None, 689) -> dict[str, Any]: 690 if isinstance(inference, Bayesian): 691 return _bayesian_inference_kwargs(inference) 692 if isinstance(inference, Frequentist) or inference is None: 693 return {} 694 return {} 695 696 697def _wrap_temporal(raw: TemporalAnalysisResult) -> AnalysisResult: 698 # Mirror static ate_result_from_analysis: never claim pass when nothing ran. 699 ran = raw.refutation_count > 0 700 mediation = None 701 if getattr(raw, "mediation_total", None) is not None or getattr(raw, "mediation_mediated", None) is not None: 702 mediation = MediationView( 703 total=getattr(raw, "mediation_total", None), 704 direct=getattr(raw, "mediation_direct", None), 705 mediated=getattr(raw, "mediation_mediated", None), 706 ) 707 posterior = None 708 if getattr(raw, "posterior_n_draws", None) is not None: 709 mass = getattr(raw, "posterior_unidentified_mass", None) 710 envelope = None 711 if mass is not None and float(mass) > 0.0: 712 envelope = EffectEnvelope( 713 effect_mean=raw.posterior_effect_mean, 714 effect_sd=raw.posterior_effect_sd, 715 q025=raw.posterior_q025, 716 q975=raw.posterior_q975, 717 unidentified_mass=float(mass), 718 n_draws=raw.posterior_n_draws, 719 backend=raw.posterior_backend, 720 ) 721 posterior = PosteriorView( 722 effect_mean=raw.posterior_effect_mean, 723 effect_sd=raw.posterior_effect_sd, 724 q025=raw.posterior_q025, 725 q975=raw.posterior_q975, 726 n_draws=raw.posterior_n_draws, 727 p_below_zero=raw.posterior_p_below_zero, 728 backend=raw.posterior_backend, 729 artifact=raw.posterior_artifact, 730 unidentified_mass=None if mass is None else float(mass), 731 envelope=envelope, 732 ) 733 return AnalysisResult( 734 identification=IdentificationView( 735 status=raw.identification_status, 736 method=raw.method, 737 adjustment_set=list(getattr(raw, "adjustment_set", []) or []), 738 assumption_count=int(getattr(raw, "assumption_count", 0) or 0), 739 derivation_step_count=int(getattr(raw, "derivation_step_count", 0) or 0), 740 ), 741 estimate=EstimateView( 742 ate=raw.ate, 743 se_analytic=raw.se_analytic, 744 se_bootstrap=raw.se_bootstrap, 745 estimator_id=str(getattr(raw, "estimator_id", "") or ""), 746 method=raw.method, 747 mediation=mediation, 748 ), 749 posterior=posterior, 750 mediation=mediation, 751 validation=ValidationView( 752 passed=False if not ran else True, 753 ran=ran, 754 count=raw.refutation_count, 755 ), 756 performance=PerformanceView( 757 plan_id=raw.plan_id, 758 modality=raw.modality, 759 peak_memory_bytes=raw.peak_memory_bytes, 760 ), 761 diagnostics=list(raw.diagnostics), 762 provenance={ 763 "node_count": raw.provenance_node_count, 764 "worker_threads": getattr(raw, "worker_threads", None), 765 "expected_python_crossings": getattr(raw, "expected_python_crossings", None), 766 }, 767 plan=_plan_from_raw(raw), 768 _raw=raw, 769 ) 770 771 772 773def _resolve_static_discovery_edges(data, discovery, accept_discovered: bool, seed: int, threads: int): 774 """Run static discovery and return oriented DAG edge list. 775 776 When ``accept_discovered`` is True, incomplete CPDAG/PAG marks raise 777 ``ValueError`` (auto-accept cannot invent orientations). When False, 778 raises ``CausalReviewError`` with structured attrs. 779 """ 780 from . import discovery as disc 781 from ._native import CausalReviewError 782 783 def _require_oriented(result, *, kind: str, algorithm: str): 784 try: 785 return list(discovery_to_dag(result).edges()) 786 except ValueError as exc: 787 pending = sum( 788 1 789 for e in result.graph_edges 790 if not ( 791 (e.at_source == "tail" and e.at_target == "arrow") 792 or (e.at_source == "arrow" and e.at_target == "tail") 793 ) 794 ) 795 if accept_discovered: 796 raise ValueError( 797 f"{algorithm}: accept_discovered=True but graph is incomplete " 798 f"({pending} non-directed marks); cannot invent orientations. {exc}" 799 ) from exc 800 err = CausalReviewError( 801 "cannot execute while graph review is required" 802 ) 803 err.kind = kind 804 err.algorithm = algorithm 805 err.pending_edge_count = pending 806 err.hint = ( 807 "orient remaining edges into a Dag, or use finish_*_review / " 808 "supply graph= edges" 809 ) 810 err.message = str(err) 811 raise err from exc 812 813 if isinstance(discovery, ExactDagPosterior): 814 return graph_posterior_map_edges(disc.discover_exact_dag_posterior(data)) 815 if isinstance(discovery, OrderMcmc): 816 return graph_posterior_map_edges( 817 disc.discover_order_mcmc( 818 data, 819 n_warmup=discovery.n_warmup, 820 n_draws=discovery.n_draws, 821 seed=seed, 822 threads=threads, 823 ) 824 ) 825 if isinstance(discovery, StructureMcmc): 826 return graph_posterior_map_edges( 827 disc.discover_structure_mcmc( 828 data, 829 n_warmup=discovery.n_warmup, 830 n_draws=discovery.n_draws, 831 seed=seed, 832 threads=threads, 833 ) 834 ) 835 if isinstance(discovery, CiScreenedPosterior): 836 return graph_posterior_map_edges( 837 disc.discover_ci_screened_posterior( 838 data, 839 alpha=discovery.alpha, 840 fdr=discovery.fdr, 841 seed=seed, 842 threads=threads, 843 ) 844 ) 845 if isinstance(discovery, PC): 846 result = disc.discover_pc( 847 data, alpha=discovery.alpha, fdr=discovery.fdr, seed=seed, threads=threads 848 ) 849 return _require_oriented(result, kind="static_cpdag", algorithm="pc") 850 if isinstance(discovery, GES): 851 result = disc.discover_ges( 852 data, alpha=discovery.alpha, fdr=discovery.fdr, seed=seed, threads=threads 853 ) 854 return _require_oriented(result, kind="static_cpdag", algorithm="ges") 855 if isinstance(discovery, LiNGAM): 856 result = disc.discover_lingam(data, seed=seed, threads=threads) 857 return _require_oriented(result, kind="static_dag", algorithm="lingam") 858 if isinstance(discovery, NOTEARS): 859 result = disc.discover_notears(data, seed=seed, threads=threads) 860 return _require_oriented(result, kind="static_dag", algorithm="notears") 861 if isinstance(discovery, (FCI, RFCI)): 862 algo = "fci" if isinstance(discovery, FCI) else "rfci" 863 if not accept_discovered: 864 err = CausalReviewError( 865 "FCI/RFCI PathSpecific/Interventional queries require a fully " 866 "oriented DAG; accept_discovered=False leaves PAG review open" 867 ) 868 err.kind = "static_pag" 869 err.algorithm = algo 870 err.pending_edge_count = 0 871 err.hint = ( 872 "orient the PAG to a Dag (or use PC/GES/LiNGAM/NOTEARS); " 873 "PathSpecific/Interventional do not run generalized PAG adjustment" 874 ) 875 err.message = str(err) 876 raise err 877 raise ValueError( 878 f"{algo}: PathSpecific/Interventional require a fully oriented DAG; " 879 "use PC/GES/LiNGAM/NOTEARS or supply graph= edges " 880 "(accept_discovered cannot invent PAG orientations)" 881 ) 882 raise TypeError(f"unsupported discovery type for path/distribution: {type(discovery)!r}") 883 884 885def analyze_many( 886 data: Mapping[str, Any] | Any, 887 *, 888 graph: Dag | Sequence[tuple[str, str]], 889 queries: Sequence[AverageEffect], 890 identifier: str | None = None, 891 estimator: str | None = None, 892 refute: bool | Literal["full", "placebo", "none", "cheap"] = True, 893 seed: int = 1, 894 bootstrap: int | None = None, 895 threads: int = 1, 896 latency: Literal["interactive", "standard", "report"] | None = None, 897) -> list[AnalysisResult]: 898 """Estimate many average effects on one shared table ingest. 899 900 Parameters 901 ---------- 902 data: 903 Column mapping / DataFrame (ingested once). 904 graph: 905 Static DAG or edge list shared by every query. 906 queries: 907 Non-empty sequence of ``AverageEffect`` queries. 908 """ 909 if not queries: 910 raise ValueError("analyze_many requires at least one query") 911 if not all(isinstance(q, AverageEffect) for q in queries): 912 raise TypeError("analyze_many currently supports AverageEffect queries only") 913 bootstrap, refute = _resolve_latency_budget(latency, bootstrap, refute) 914 names, columns = as_columns(data) # type: ignore[arg-type] 915 edges = _static_edges(graph) # type: ignore[arg-type] 916 specs = [ 917 (q.treatment, q.outcome, float(q.control_level), float(q.active_level)) 918 for q in queries 919 ] 920 kwargs: dict[str, Any] = dict( 921 identifier=identifier, 922 estimator=estimator, 923 refute=refute, 924 seed=seed, 925 bootstrap=bootstrap, 926 threads=threads, 927 ) 928 if latency is not None: 929 kwargs["latency"] = latency 930 raws = _analyze_ate_many(names, columns, edges, specs, **kwargs) 931 return [_wrap_ate(r) for r in raws] 932 933 934@dataclass(frozen=True) 935class IdentifyResult: 936 """Identify-only result (no estimate).""" 937 938 status: str 939 method: str 940 adjustment_set: list[str] 941 942 943def identify( 944 *, 945 graph: Dag | Sequence[tuple[str, str]], 946 query: AverageEffect, 947 names: Sequence[str] | None = None, 948 identifier: str | Identifier | None = None, 949) -> IdentifyResult: 950 """Identify without estimating. 951 952 Pass ``names`` when ``graph`` is an edge list (variable order). With a 953 ``Dag``, names are taken from ``graph.nodes()``. 954 """ 955 if isinstance(identifier, Identifier): 956 identifier = str(identifier) 957 if isinstance(graph, Dag): 958 node_names = list(graph.nodes()) 959 edges = list(graph.edges()) 960 else: 961 if names is None: 962 raise ValueError("identify(edge_list) requires names=") 963 node_names = list(names) 964 edges = list(graph) 965 status, method, adjustment = _identify_ate( 966 node_names, 967 edges, 968 query.treatment, 969 query.outcome, 970 identifier=identifier, 971 ) 972 return IdentifyResult(status=status, method=method, adjustment_set=list(adjustment)) 973 974 975def analyze( 976 data: Mapping[str, Any] | Any | Sequence[Mapping[str, Any] | Any], 977 *, 978 query: ( 979 AverageEffect 980 | PulseEffect 981 | SustainedEffect 982 | InterventionalDistribution 983 | PathSpecificEffect 984 | ConditionalEffect 985 | MediationEffect 986 | Counterfactual 987 | TemporalMediationEffect 988 ), 989 graph: ( 990 Dag 991 | Cpdag 992 | Pag 993 | Admg 994 | TemporalDag 995 | TemporalCpdag 996 | TemporalPag 997 | Sequence[tuple[str, str]] 998 | Sequence[tuple[str, int, str, int]] 999 | None 1000 ) = None, 1001 discovery: Any | None = None, 1002 inference: Frequentist | Bayesian | None = None, 1003 identifier: str | Identifier | None = None, 1004 estimator: str | Estimator | None = None, 1005 refute: bool | Refute | Literal["full", "placebo", "none", "cheap"] = True, 1006 validators: Sequence[Any] | None = None, 1007 accept_discovered: bool = True, 1008 seed: int = 1, 1009 bootstrap: int | None = None, 1010 threads: int = 1, 1011 regimes: Sequence[int] | None = None, 1012 running_variable: str | None = None, 1013 cutoff: float | None = None, 1014 bandwidth: float | None = None, 1015 population_registry: Any | None = None, 1016 latency: Latency | Literal["interactive", "standard", "report"] | None = None, 1017 cancel: Any | None = None, 1018 on_progress: Any | None = None, 1019 on_stage: Any | None = None, 1020 return_posterior_artifact: bool = False, 1021) -> AnalysisResult: 1022 """Identify then estimate a causal effect. 1023 1024 Parameters 1025 ---------- 1026 data: 1027 Mapping of column name → 1-d float array, a pandas ``DataFrame``, 1028 Arrow CDI exporters (PyArrow columns / table), or a 1029 ``antecedent.data`` frame (``EventFrame`` / ``PanelFrame`` / ``MultiEnvFrame``). 1030 For ``discovery=JPCMCIPlus(...)``, pass a sequence of environment frames 1031 or a ``MultiEnvFrame``. 1032 query: 1033 ``AverageEffect``, ``PulseEffect`` / ``SustainedEffect``, 1034 ``InterventionalDistribution``, ``PathSpecificEffect``, 1035 ``MediationEffect``, ``Counterfactual``, or ``TemporalMediationEffect``. 1036 graph: 1037 ``Dag`` / ``Cpdag`` / ``Pag`` / ``Admg`` / ``TemporalDag`` / 1038 ``TemporalCpdag`` / ``TemporalPag``, or an edge list. Lagged edges 1039 ``(from, from_lag, to, to_lag)`` are required for temporal queries 1040 without ``discovery``. Fully oriented CPDAGs run as DAGs; incomplete 1041 CPDAGs require review. ADMGs without bidirected edges coerce to DAGs; 1042 ADMGs with latents use general ID + functional effect. 1043 discovery: 1044 Static: ``PC`` / ``GES`` / ``LiNGAM`` / ``NOTEARS`` / ``FCI`` / ``RFCI``. 1045 Temporal: ``PCMCI`` / ``PCMCIPlus`` / ``LPCMCI`` / ``JPCMCIPlus`` / ``RPCMCI``. 1046 One-shot script convenience — discovery runs at compile time. For 1047 interactive / spreadsheet estimate clicks, discover once into 1048 :class:`antecedent.AcceptedGraph` (or hold a reviewed graph) and pass 1049 ``graph=`` with ``latency="interactive"`` instead. Combining 1050 ``discovery=`` with ``latency="interactive"`` raises 1051 :class:`CausalUnsupportedError`. 1052 latency: 1053 Optional compute tier (``interactive`` / ``standard`` / ``report``). 1054 Maps to known-equivalent bootstrap / refute / draws; explicit 1055 ``bootstrap=`` / ``refute=`` always win. Interactive refuses inline 1056 ``discovery=`` (artifact-first UX). 1057 cancel: 1058 Optional ``CancellationToken`` from ``antecedent._native``. 1059 on_progress: 1060 Optional ``(fraction: float, stage: str) -> None`` callback. 1061 on_stage: 1062 Optional ``(stage: str, payload: dict) -> None`` progressive stage 1063 callback (identify → estimate_point → uncertainty → validate). 1064 return_posterior_artifact: 1065 When ``True`` and inference is Bayesian, attach full posterior draw 1066 bytes on ``result.posterior.artifact`` (for download / sequential-prior 1067 hydrate). Default ``False``: UI summaries only. 1068 """ 1069 if isinstance(identifier, Identifier): 1070 identifier = str(identifier) 1071 if isinstance(estimator, Estimator): 1072 estimator = str(estimator) 1073 if isinstance(latency, Latency): 1074 latency = str(latency) # type: ignore[assignment] 1075 if isinstance(refute, Refute): 1076 refute = str(refute) # type: ignore[assignment] 1077 inference = inference or Frequentist() 1078 bootstrap, refute = _resolve_latency_budget(latency, bootstrap, refute) 1079 1080 if discovery is not None and latency == "interactive": 1081 raise CausalUnsupportedError( 1082 "discovery= is not on the interactive estimate path; " 1083 "call discover_* once, accept into AcceptedGraph, then " 1084 "analyze(graph=..., latency='interactive')" 1085 ) 1086 1087 if isinstance(query, ConditionalEffect): 1088 if isinstance(inference, Bayesian): 1089 raise TypeError("ConditionalEffect does not support inference=Bayesian(...)") 1090 if discovery is not None: 1091 raise ValueError("ConditionalEffect does not support discovery=") 1092 names, columns = as_columns(data) # type: ignore[arg-type] 1093 edges = _static_edges(graph) # type: ignore[arg-type] 1094 raw = _analyze_conditional( 1095 names, 1096 columns, 1097 edges, 1098 query.treatment, 1099 query.outcome, 1100 query.modifier, 1101 control_level=query.control_level, 1102 active_level=query.active_level, 1103 refute=refute, 1104 validators=list(validators) if validators is not None else None, 1105 seed=seed, 1106 bootstrap=bootstrap, 1107 threads=threads, 1108 ) 1109 return _wrap_ate(raw) 1110 1111 if isinstance(query, TemporalMediationEffect): 1112 if isinstance(inference, Bayesian): 1113 raise TypeError("TemporalMediationEffect does not support inference=Bayesian(...)") 1114 if discovery is not None: 1115 raise ValueError("TemporalMediationEffect does not support discovery=") 1116 names, columns = as_columns(data) # type: ignore[arg-type] 1117 lagged = _lagged_edges(graph) # type: ignore[arg-type] 1118 raw = _analyze_temporal_mediation( 1119 names, 1120 columns, 1121 lagged, 1122 query.treatment, 1123 query.mediator, 1124 query.outcome, 1125 contrast=query.contrast, 1126 control_level=query.control_level, 1127 active_level=query.active_level, 1128 seed=seed, 1129 bootstrap=bootstrap, 1130 threads=threads, 1131 ) 1132 return _wrap_temporal(raw) 1133 1134 if isinstance(query, MediationEffect): 1135 if discovery is not None: 1136 raise ValueError("MediationEffect does not support discovery=") 1137 edges = _static_edges(graph) # type: ignore[arg-type] 1138 names, columns = as_columns(data) # type: ignore[arg-type] 1139 raw = _analyze_mediation( 1140 names, 1141 columns, 1142 edges, 1143 query.treatment, 1144 query.outcome, 1145 list(query.mediators), 1146 contrast=query.contrast, 1147 control_level=query.control_level, 1148 active_level=query.active_level, 1149 refute=refute, 1150 seed=seed, 1151 bootstrap=bootstrap, 1152 threads=threads, 1153 ) 1154 return _wrap_ate(raw) 1155 1156 if isinstance(query, Counterfactual): 1157 from ._native import counterfactual_ite 1158 1159 if discovery is not None: 1160 raise ValueError("Counterfactual does not support discovery=") 1161 edges = _static_edges(graph) # type: ignore[arg-type] 1162 names, columns = as_columns(data) # type: ignore[arg-type] 1163 ite = counterfactual_ite( 1164 names, 1165 columns, 1166 edges, 1167 query.treatment, 1168 query.outcome, 1169 query.active_level, 1170 query.control_level, 1171 seed=seed, 1172 threads=threads, 1173 ) 1174 return AnalysisResult( 1175 identification=IdentificationView( 1176 status="gcm.parametric", 1177 method="counterfactual.ite", 1178 adjustment_set=[], 1179 assumption_count=0, 1180 derivation_step_count=0, 1181 ), 1182 estimate=EstimateView( 1183 ate=float(ite.mean_ite), 1184 se_analytic=float("nan"), 1185 se_bootstrap=None, 1186 estimator_id="gcm.ite", 1187 method="counterfactual.ite", 1188 ), 1189 posterior=None, 1190 validation=ValidationView(passed=False, ran=False, count=0), 1191 performance=PerformanceView( 1192 plan_id="counterfactual.ite", 1193 modality="static", 1194 peak_memory_bytes=0, 1195 ), 1196 diagnostics=[], 1197 provenance={"noise_inference": getattr(ite, "noise_inference", None)}, 1198 _raw=ite, 1199 ) 1200 1201 if isinstance(query, InterventionalDistribution): 1202 if discovery is not None: 1203 edges = _resolve_static_discovery_edges( 1204 data, discovery, accept_discovered, seed, threads 1205 ) 1206 else: 1207 edges = _static_edges(graph) # type: ignore[arg-type] 1208 names, columns = as_columns(data) # type: ignore[arg-type] 1209 raw = _analyze_distribution( 1210 names, 1211 columns, 1212 edges, 1213 query.outcome, 1214 dict(query.interventions), 1215 conditioning=list(query.conditioning) or None, 1216 seed=seed, 1217 threads=threads, 1218 ) 1219 return _wrap_ate(raw) 1220 1221 if isinstance(query, PathSpecificEffect): 1222 if discovery is not None: 1223 edges = _resolve_static_discovery_edges( 1224 data, discovery, accept_discovered, seed, threads 1225 ) 1226 else: 1227 edges = _static_edges(graph) # type: ignore[arg-type] 1228 names, columns = as_columns(data) # type: ignore[arg-type] 1229 raw = _analyze_path_specific( 1230 names, 1231 columns, 1232 edges, 1233 query.treatment, 1234 query.outcome, 1235 control_level=query.control_level, 1236 active_level=query.active_level, 1237 path_nodes=list(query.path_nodes) if query.path_nodes is not None else None, 1238 max_paths=query.max_paths, 1239 max_len=query.max_len, 1240 seed=seed, 1241 bootstrap=bootstrap, 1242 threads=threads, 1243 ) 1244 return _wrap_ate(raw) 1245 1246 if discovery is not None and isinstance( 1247 discovery, _STATIC_DISCOVERY + _GRAPH_POSTERIOR_DISCOVERY 1248 ): 1249 if not isinstance(query, AverageEffect): 1250 raise ValueError( 1251 f"discovery={type(discovery).__name__}(...) requires AverageEffect" 1252 ) 1253 if isinstance(discovery, _GRAPH_POSTERIOR_DISCOVERY) and not isinstance( 1254 inference, Bayesian 1255 ): 1256 raise TypeError( 1257 "graph-posterior discovery requires inference=Bayesian(...) " 1258 "for effect mixture" 1259 ) 1260 names, columns = as_columns(data) # type: ignore[arg-type] 1261 cfg = _discovery_algorithm(discovery) 1262 bayes_kw: dict[str, Any] = {} 1263 if isinstance(inference, Bayesian): 1264 bayes_kw = _bayesian_inference_kwargs(inference) 1265 raw = _analyze_ate_discover( 1266 names, 1267 columns, 1268 query.treatment, 1269 query.outcome, 1270 algorithm=cfg["algorithm"], 1271 alpha=cfg.get("alpha", 0.05), 1272 fdr=cfg.get("fdr", True), 1273 max_cond_size=cfg.get("max_cond_size", 2), 1274 prune_threshold=cfg.get("prune_threshold", 0.0), 1275 l1=cfg.get("lambda", 0.1), 1276 threshold=cfg.get("threshold", 0.3), 1277 standardize=cfg.get("standardize", True), 1278 accept_discovered=accept_discovered, 1279 control_level=query.control_level, 1280 active_level=query.active_level, 1281 identifier=identifier, 1282 estimator=estimator, 1283 refute=refute, 1284 validators=list(validators) if validators is not None else None, 1285 ci=cfg.get("ci"), 1286 n_chains=cfg.get("n_chains", 2), 1287 n_warmup=cfg.get("n_warmup", 100), 1288 mcmc_draws=cfg.get("mcmc_draws", 200), 1289 thin=cfg.get("thin", 1), 1290 soft_weight=cfg.get("soft_weight", "none"), 1291 require_diagnostics_gate=cfg.get("require_diagnostics_gate", True), 1292 seed=seed, 1293 bootstrap=bootstrap, 1294 threads=threads, 1295 **bayes_kw, 1296 ) 1297 return _wrap_ate(raw) 1298 1299 if discovery is not None and isinstance(query, AverageEffect): 1300 raise ValueError( 1301 "AverageEffect with discovery= requires a static algorithm " 1302 "(PC/GES/LiNGAM/NOTEARS/FCI/RFCI); temporal discovery needs " 1303 "PulseEffect/SustainedEffect" 1304 ) 1305 1306 if isinstance(query, AverageEffect): 1307 bayes_kw: dict[str, Any] = {} 1308 if isinstance(inference, Bayesian): 1309 bayes_kw = _bayesian_inference_kwargs(inference) 1310 if estimator == "rd.sharp" or any( 1311 v is not None for v in (running_variable, cutoff, bandwidth) 1312 ): 1313 if running_variable is None or cutoff is None or bandwidth is None: 1314 raise ValueError( 1315 "rd.sharp (or any RD kwargs) requires running_variable, cutoff, and bandwidth" 1316 ) 1317 if estimator is None: 1318 estimator = "rd.sharp" 1319 if identifier is None: 1320 identifier = "rd.sharp" 1321 common = dict( 1322 treatment=query.treatment, 1323 outcome=query.outcome, 1324 control_level=query.control_level, 1325 active_level=query.active_level, 1326 identifier=identifier, 1327 estimator=estimator, 1328 refute=refute, 1329 validators=list(validators) if validators is not None else None, 1330 running_variable=running_variable, 1331 cutoff=cutoff, 1332 bandwidth=bandwidth, 1333 seed=seed, 1334 bootstrap=bootstrap, 1335 threads=threads, 1336 **bayes_kw, 1337 ) 1338 if return_posterior_artifact: 1339 common["return_posterior_artifact"] = True 1340 if latency is not None: 1341 common["latency"] = latency 1342 if cancel is not None: 1343 common["cancel"] = cancel 1344 if on_progress is not None: 1345 common["on_progress"] = on_progress 1346 if on_stage is not None: 1347 common["on_stage"] = on_stage 1348 from .population import coerce_target_population, registry_wire 1349 1350 pop = coerce_target_population( 1351 getattr(query, "target_population", None) 1352 ) 1353 preds, dists = registry_wire(population_registry) 1354 pop_kw: dict[str, Any] = {} 1355 if pop is not None: 1356 pop_kw["target_population"] = pop 1357 if preds: 1358 pop_kw["population_predicates"] = preds 1359 if dists: 1360 pop_kw["population_distributions"] = dists 1361 if pop_kw and isinstance(graph, (Pag, Cpdag, Admg)): 1362 raise ValueError( 1363 "target_population / population_registry currently require a Dag " 1364 "(or edge list); PAG/CPDAG/ADMG analyze paths do not accept them yet" 1365 ) 1366 if isinstance(graph, Pag): 1367 names, columns = as_columns(data) # type: ignore[arg-type] 1368 return _wrap_ate(_analyze_ate_pag(names, columns, graph, **common)) 1369 if isinstance(graph, Cpdag): 1370 names, columns = as_columns(data) # type: ignore[arg-type] 1371 return _wrap_ate(_analyze_ate_cpdag(names, columns, graph, **common)) 1372 if isinstance(graph, Admg): 1373 names, columns = as_columns(data) # type: ignore[arg-type] 1374 return _wrap_ate(_analyze_ate_admg(names, columns, graph, **common)) 1375 edges = _static_edges(graph) # type: ignore[arg-type] 1376 arrow = try_as_arrow_c_columns(data) 1377 ate_kwargs = dict(edges=edges, **common, **pop_kw) 1378 # Prefer Arrow CDI (zero-copy float64) when available. Population kwargs 1379 # still require the NumPy path until that surface is wired on CDI. 1380 use_arrow = arrow is not None and not pop_kw 1381 if use_arrow: 1382 names, columns = arrow 1383 raw = _analyze_ate_arrow_c(names, columns, **ate_kwargs) 1384 else: 1385 names, columns = as_columns(data) # type: ignore[arg-type] 1386 raw = _analyze_ate(names, columns, **ate_kwargs) 1387 return _wrap_ate(raw) 1388 1389 if isinstance(query, (PulseEffect, SustainedEffect)): 1390 policy = "sustained" if isinstance(query, SustainedEffect) else "pulse" 1391 _reject_unsupported_temporal( 1392 inference=inference, refute=refute, validators=validators 1393 ) 1394 bayes_kw = _temporal_inference_kwargs(inference) 1395 if isinstance(data, EventFrame): 1396 if discovery is not None: 1397 if isinstance(discovery, JPCMCIPlus): 1398 raise TypeError( 1399 "EventFrame does not support discovery=JPCMCIPlus(...); " 1400 "use MultiEnvFrame or PanelFrame for multi-environment discovery" 1401 ) 1402 if isinstance(discovery, DbnPosterior): 1403 if not isinstance(inference, Bayesian): 1404 raise TypeError( 1405 "EventFrame discovery=DbnPosterior(...) requires inference=Bayesian(...)" 1406 ) 1407 elif not isinstance(discovery, (PCMCI, PCMCIPlus, LPCMCI, RPCMCI)): 1408 raise TypeError( 1409 f"EventFrame discovery expects PCMCI/PCMCIPlus/LPCMCI/RPCMCI/DbnPosterior, " 1410 f"got {type(discovery)!r}" 1411 ) 1412 cfg = _discovery_algorithm(discovery) 1413 raw = _analyze_events( 1414 data.names, 1415 data.columns, 1416 data.event_times_ns.tolist(), 1417 data.align_interval_ns, 1418 [], # discovery path ignores edges 1419 query.treatment, 1420 query.outcome, 1421 treatment_lag=query.treatment_lag, 1422 horizon_steps=query.horizon_steps, 1423 active_level=query.active_level, 1424 policy=policy, 1425 **bayes_kw, 1426 refute=refute, 1427 validators=list(validators) if validators is not None else None, 1428 seed=seed, 1429 bootstrap=bootstrap, 1430 threads=threads, 1431 algorithm=cfg["algorithm"], 1432 max_lag=cfg.get("max_lag", 1), 1433 alpha=cfg.get("alpha", 0.05), 1434 fdr=cfg.get("fdr", True), 1435 accept_discovered=accept_discovered, 1436 regimes=list(regimes) if regimes is not None else None, 1437 **{ 1438 k: cfg[k] 1439 for k in ("n_chains", "n_warmup", "mcmc_draws", "force_mcmc", "ci") 1440 if k in cfg 1441 }, 1442 ) 1443 return _wrap_temporal(raw) 1444 lagged = _lagged_edges(graph) # type: ignore[arg-type] 1445 raw = _analyze_events( 1446 data.names, 1447 data.columns, 1448 data.event_times_ns.tolist(), 1449 data.align_interval_ns, 1450 lagged, 1451 query.treatment, 1452 query.outcome, 1453 treatment_lag=query.treatment_lag, 1454 horizon_steps=query.horizon_steps, 1455 active_level=query.active_level, 1456 policy=policy, 1457 **bayes_kw, 1458 refute=refute, 1459 validators=list(validators) if validators is not None else None, 1460 seed=seed, 1461 bootstrap=bootstrap, 1462 threads=threads, 1463 ) 1464 return _wrap_temporal(raw) 1465 if isinstance(data, PanelFrame): 1466 if discovery is not None: 1467 if isinstance(discovery, JPCMCIPlus): 1468 cfg = _discovery_algorithm(discovery) 1469 raw = _analyze_panel_discover( 1470 data.names, 1471 data.unit_columns, 1472 data.unit_ids, 1473 query.treatment, 1474 query.outcome, 1475 max_lag=cfg["max_lag"], 1476 alpha=cfg["alpha"], 1477 fdr=cfg["fdr"], 1478 accept_discovered=accept_discovered, 1479 treatment_lag=query.treatment_lag, 1480 horizon_steps=query.horizon_steps, 1481 active_level=query.active_level, 1482 policy=policy, 1483 **bayes_kw, 1484 refute=refute, 1485 validators=list(validators) if validators is not None else None, 1486 seed=seed, 1487 bootstrap=bootstrap, 1488 threads=threads, 1489 context_names=cfg["context_names"], 1490 include_space_dummy=cfg["include_space_dummy"], 1491 include_time_dummy=cfg["include_time_dummy"], 1492 space_dummy_ci=cfg["space_dummy_ci"] 1493 in ("multivariate", "multivariate_block", "block", True), 1494 time_dummy_encoding=cfg["time_dummy_encoding"], 1495 time_dummy_ci=cfg["time_dummy_ci"] 1496 in ("multivariate", "multivariate_block", "block", True), 1497 ) 1498 return _wrap_temporal(raw) 1499 if isinstance(discovery, (PCMCI, PCMCIPlus, LPCMCI)): 1500 cfg = _discovery_algorithm(discovery) 1501 # Pooled-units discovery: treat panel as multi-env without JPCMCI+ context. 1502 raw = _analyze_panel_discover( 1503 data.names, 1504 data.unit_columns, 1505 data.unit_ids, 1506 query.treatment, 1507 query.outcome, 1508 max_lag=cfg["max_lag"], 1509 alpha=cfg["alpha"], 1510 fdr=cfg["fdr"], 1511 accept_discovered=accept_discovered, 1512 treatment_lag=query.treatment_lag, 1513 horizon_steps=query.horizon_steps, 1514 active_level=query.active_level, 1515 policy=policy, 1516 **bayes_kw, 1517 refute=refute, 1518 validators=list(validators) if validators is not None else None, 1519 seed=seed, 1520 bootstrap=bootstrap, 1521 threads=threads, 1522 algorithm=cfg["algorithm"], 1523 ) 1524 return _wrap_temporal(raw) 1525 raise TypeError( 1526 "PanelFrame discovery supports JPCMCIPlus, PCMCI, PCMCIPlus, or LPCMCI" 1527 ) 1528 lagged = _lagged_edges(graph) # type: ignore[arg-type] 1529 raw = _analyze_panel( 1530 data.names, 1531 data.unit_columns, 1532 data.unit_ids, 1533 lagged, 1534 query.treatment, 1535 query.outcome, 1536 treatment_lag=query.treatment_lag, 1537 horizon_steps=query.horizon_steps, 1538 active_level=query.active_level, 1539 policy=policy, 1540 **bayes_kw, 1541 refute=refute, 1542 validators=list(validators) if validators is not None else None, 1543 seed=seed, 1544 bootstrap=bootstrap, 1545 threads=threads, 1546 ) 1547 return _wrap_temporal(raw) 1548 if isinstance(data, MultiEnvFrame): 1549 if discovery is None or not isinstance(discovery, JPCMCIPlus): 1550 raise TypeError( 1551 "MultiEnvFrame requires discovery=JPCMCIPlus(...)" 1552 ) 1553 cfg = _discovery_algorithm(discovery) 1554 raw = _analyze_temporal_discover( 1555 data.names, 1556 data.env_columns[0], 1557 query.treatment, 1558 query.outcome, 1559 algorithm="jpcmci_plus", 1560 max_lag=cfg["max_lag"], 1561 alpha=cfg["alpha"], 1562 fdr=cfg["fdr"], 1563 accept_discovered=accept_discovered, 1564 treatment_lag=query.treatment_lag, 1565 horizon_steps=query.horizon_steps, 1566 active_level=query.active_level, 1567 policy=policy, 1568 **bayes_kw, 1569 seed=seed, 1570 bootstrap=bootstrap, 1571 threads=threads, 1572 env_columns=data.env_columns, 1573 context_names=cfg["context_names"], 1574 include_space_dummy=cfg["include_space_dummy"], 1575 include_time_dummy=cfg["include_time_dummy"], 1576 space_dummy_ci=cfg["space_dummy_ci"], 1577 time_dummy_encoding=cfg["time_dummy_encoding"], 1578 time_dummy_ci=cfg["time_dummy_ci"], 1579 ci=cfg.get("ci"), 1580 ) 1581 return _wrap_temporal(raw) 1582 if discovery is not None: 1583 if isinstance(discovery, DbnPosterior): 1584 if not isinstance(inference, Bayesian): 1585 raise TypeError( 1586 "discovery=DbnPosterior(...) requires inference=Bayesian(...) " 1587 "for temporal effect mixture" 1588 ) 1589 cfg = _discovery_algorithm(discovery) 1590 names, columns = as_columns(data) # type: ignore[arg-type] 1591 raw = _analyze_temporal_discover( 1592 names, 1593 columns, 1594 query.treatment, 1595 query.outcome, 1596 algorithm="dbn_posterior", 1597 max_lag=cfg["max_lag"], 1598 accept_discovered=accept_discovered, 1599 treatment_lag=query.treatment_lag, 1600 horizon_steps=query.horizon_steps, 1601 active_level=query.active_level, 1602 policy=policy, 1603 **bayes_kw, 1604 n_chains=cfg["n_chains"], 1605 n_warmup=cfg["n_warmup"], 1606 mcmc_draws=cfg["mcmc_draws"], 1607 force_mcmc=cfg["force_mcmc"], 1608 seed=seed, 1609 bootstrap=bootstrap, 1610 threads=threads, 1611 ) 1612 return _wrap_temporal(raw) 1613 if not isinstance(discovery, _TEMPORAL_DISCOVERY): 1614 raise TypeError( 1615 f"temporal discovery expects PCMCI-family or DbnPosterior, got {type(discovery)!r}" 1616 ) 1617 cfg = _discovery_algorithm(discovery) 1618 algo = cfg["algorithm"] 1619 if algo == "jpcmci_plus": 1620 if not isinstance(data, Sequence) or isinstance(data, (str, bytes, Mapping)): 1621 raise TypeError( 1622 "discovery=JPCMCIPlus(...) requires data as a sequence of " 1623 "environment mappings/DataFrames" 1624 ) 1625 names, env_columns = as_multi_env_columns(data) 1626 raw = _analyze_temporal_discover( 1627 names, 1628 env_columns[0], 1629 query.treatment, 1630 query.outcome, 1631 algorithm=algo, 1632 max_lag=cfg["max_lag"], 1633 alpha=cfg["alpha"], 1634 fdr=cfg["fdr"], 1635 accept_discovered=accept_discovered, 1636 treatment_lag=query.treatment_lag, 1637 horizon_steps=query.horizon_steps, 1638 active_level=query.active_level, 1639 policy=policy, 1640 **bayes_kw, 1641 seed=seed, 1642 bootstrap=bootstrap, 1643 threads=threads, 1644 env_columns=env_columns, 1645 context_names=cfg["context_names"], 1646 include_space_dummy=cfg["include_space_dummy"], 1647 include_time_dummy=cfg["include_time_dummy"], 1648 space_dummy_ci=cfg["space_dummy_ci"], 1649 time_dummy_encoding=cfg["time_dummy_encoding"], 1650 time_dummy_ci=cfg["time_dummy_ci"], 1651 ci=cfg.get("ci"), 1652 ) 1653 return _wrap_temporal(raw) 1654 if algo == "rpcmci": 1655 if regimes is None: 1656 raise ValueError("discovery=RPCMCI(...) requires regimes=[…] labels") 1657 names, columns = as_columns(data) # type: ignore[arg-type] 1658 raw = _analyze_temporal_discover( 1659 names, 1660 columns, 1661 query.treatment, 1662 query.outcome, 1663 algorithm=algo, 1664 max_lag=cfg["max_lag"], 1665 alpha=cfg["alpha"], 1666 fdr=cfg["fdr"], 1667 accept_discovered=accept_discovered, 1668 treatment_lag=query.treatment_lag, 1669 horizon_steps=query.horizon_steps, 1670 active_level=query.active_level, 1671 policy=policy, 1672 **bayes_kw, 1673 seed=seed, 1674 bootstrap=bootstrap, 1675 threads=threads, 1676 regimes=list(regimes), 1677 ci=cfg.get("ci"), 1678 ) 1679 return _wrap_temporal(raw) 1680 names, columns = as_columns(data) # type: ignore[arg-type] 1681 raw = _analyze_temporal_discover( 1682 names, 1683 columns, 1684 query.treatment, 1685 query.outcome, 1686 algorithm=algo, 1687 max_lag=cfg["max_lag"], 1688 alpha=cfg["alpha"], 1689 fdr=cfg["fdr"], 1690 accept_discovered=accept_discovered, 1691 treatment_lag=query.treatment_lag, 1692 horizon_steps=query.horizon_steps, 1693 active_level=query.active_level, 1694 policy=policy, 1695 **bayes_kw, 1696 seed=seed, 1697 bootstrap=bootstrap, 1698 threads=threads, 1699 ci=cfg.get("ci"), 1700 ) 1701 return _wrap_temporal(raw) 1702 names, columns = as_columns(data) # type: ignore[arg-type] 1703 if isinstance(graph, TemporalPag): 1704 raw = _analyze_temporal_pag( 1705 names, 1706 columns, 1707 graph, 1708 query.treatment, 1709 query.outcome, 1710 treatment_lag=query.treatment_lag, 1711 horizon_steps=query.horizon_steps, 1712 active_level=query.active_level, 1713 policy=policy, 1714 **bayes_kw, 1715 refute=refute, 1716 validators=list(validators) if validators is not None else None, 1717 seed=seed, 1718 bootstrap=bootstrap, 1719 threads=threads, 1720 ) 1721 return _wrap_temporal(raw) 1722 if isinstance(graph, TemporalCpdag): 1723 try: 1724 graph = graph.try_into_temporal_dag() 1725 except Exception as exc: # noqa: BLE001 — surface orientation failures 1726 raise ValueError( 1727 "TemporalCpdag has undirected/conflict marks; orient edges " 1728 "(try_into_temporal_dag) before analyze, or use discovery review" 1729 ) from exc 1730 lagged = _lagged_edges(graph) # type: ignore[arg-type] 1731 raw = _analyze_temporal( 1732 names, 1733 columns, 1734 lagged, 1735 query.treatment, 1736 query.outcome, 1737 treatment_lag=query.treatment_lag, 1738 horizon_steps=query.horizon_steps, 1739 active_level=query.active_level, 1740 policy=policy, 1741 **bayes_kw, 1742 refute=refute, 1743 validators=list(validators) if validators is not None else None, 1744 seed=seed, 1745 bootstrap=bootstrap, 1746 threads=threads, 1747 ) 1748 return _wrap_temporal(raw) 1749 1750 raise TypeError(f"unsupported query type: {type(query)!r}") 1751 1752 1753class PreparedAnalysis: 1754 """Compile-once / re-estimate-many handle for static AverageEffect on a DAG. 1755 1756 Use for interactive sessions: prepare with a fixed graph/query/estimator, 1757 then call :meth:`estimate` or :meth:`refresh` when the table changes 1758 (same schema). Prefer this over fresh :func:`analyze` on every click. 1759 For streaming append + incremental OLS, use :class:`antecedent.CausalState`. 1760 """ 1761 1762 def __init__(self, native: Any) -> None: 1763 self._native = native 1764 1765 @classmethod 1766 def prepare( 1767 cls, 1768 data: Mapping[str, Any] | Any, 1769 *, 1770 query: AverageEffect, 1771 graph: Dag | Sequence[tuple[str, str]], 1772 inference: Frequentist | Bayesian | None = None, 1773 identifier: str | Identifier | None = None, 1774 estimator: str | Estimator | None = None, 1775 refute: bool | Refute | Literal["full", "placebo", "none", "cheap"] = False, 1776 seed: int = 1, 1777 bootstrap: int | None = None, 1778 threads: int = 1, 1779 latency: Latency | Literal["interactive", "standard", "report"] | None = "interactive", 1780 ) -> PreparedAnalysis: 1781 """Compile a durable plan for static ATE on a supplied DAG.""" 1782 if not isinstance(query, AverageEffect): 1783 raise TypeError("PreparedAnalysis supports AverageEffect only") 1784 inference = inference or Frequentist() 1785 if isinstance(identifier, Identifier): 1786 identifier = str(identifier) 1787 if isinstance(estimator, Estimator): 1788 estimator = str(estimator) 1789 if isinstance(latency, Latency): 1790 latency = str(latency) # type: ignore[assignment] 1791 if isinstance(refute, Refute): 1792 refute = str(refute) # type: ignore[assignment] 1793 bootstrap, refute = _resolve_latency_budget(latency, bootstrap, refute) 1794 names, columns = as_columns(data) # type: ignore[arg-type] 1795 edges = _static_edges(graph) # type: ignore[arg-type] 1796 bayes_kw: dict[str, Any] = {} 1797 if isinstance(inference, Bayesian): 1798 bayes_kw = _bayesian_inference_kwargs(inference) 1799 inference_mode = str(bayes_kw.pop("inference")) 1800 else: 1801 inference_mode = "frequentist" 1802 native = _NativePreparedAnalysis.prepare( 1803 names, 1804 columns, 1805 edges, 1806 query.treatment, 1807 query.outcome, 1808 control_level=query.control_level, 1809 active_level=query.active_level, 1810 identifier=identifier, 1811 estimator=estimator, 1812 inference=inference_mode, 1813 n_draws=int(bayes_kw.get("n_draws", 1000)), 1814 prior_scale=float(bayes_kw.get("prior_scale", 10.0)), 1815 refute=refute, 1816 seed=seed, 1817 bootstrap=bootstrap, 1818 threads=threads, 1819 latency=latency, 1820 ) 1821 return cls(native) 1822 1823 @property 1824 def plan(self) -> PhysicalPlanView: 1825 """Physical-plan summary retained from prepare.""" 1826 raw = self._native.plan_summary() 1827 return PhysicalPlanView( 1828 plan_id=str(raw.get("plan_id", "")), 1829 estimated_peak_memory_bytes=( 1830 int(raw["estimated_peak_memory_bytes"]) 1831 if "estimated_peak_memory_bytes" in raw 1832 else None 1833 ), 1834 workspace_bytes=( 1835 int(raw["workspace_bytes"]) if "workspace_bytes" in raw else None 1836 ), 1837 batch_size=int(raw["batch_size"]) if "batch_size" in raw else None, 1838 worker_threads=int(raw.get("worker_threads", 0)), 1839 expected_python_crossings=int(raw.get("expected_python_crossings", 0)), 1840 deterministic_reductions=str(raw.get("deterministic_reductions", "true")).lower() 1841 in ("1", "true"), 1842 kernels=raw.get("kernels") or None, 1843 ) 1844 1845 def estimate( 1846 self, 1847 data: Mapping[str, Any] | Any, 1848 *, 1849 seed: int = 1, 1850 threads: int = 1, 1851 ) -> AnalysisResult: 1852 """Re-estimate without recompiling (same schema as prepare).""" 1853 names, columns = as_columns(data) # type: ignore[arg-type] 1854 raw = self._native.estimate(names, columns, seed=seed, threads=threads) 1855 return _wrap_ate(raw, prepared=self) 1856 1857 def refresh( 1858 self, 1859 data: Mapping[str, Any] | Any, 1860 *, 1861 seed: int = 1, 1862 threads: int = 1, 1863 ) -> AnalysisResult: 1864 """Replace retained data and re-estimate.""" 1865 names, columns = as_columns(data) # type: ignore[arg-type] 1866 raw = self._native.refresh(names, columns, seed=seed, threads=threads) 1867 return _wrap_ate(raw, prepared=self) 1868 1869 def refute( 1870 self, 1871 data: Mapping[str, Any] | Any, 1872 suite: Refute | Literal["placebo", "full", "cheap"] | bool | str = "placebo", 1873 *, 1874 seed: int = 1, 1875 threads: int = 1, 1876 cancel: Any | None = None, 1877 ) -> AnalysisResult: 1878 """Second-click refute against the last :meth:`estimate` / :meth:`refresh`. 1879 1880 Interactive first clicks typically use ``refute=False`` or ``cheap``; 1881 call this with ``suite="placebo"`` or ``"full"`` for the deferred suite. 1882 """ 1883 if isinstance(suite, Refute): 1884 suite = str(suite) 1885 names, columns = as_columns(data) # type: ignore[arg-type] 1886 kwargs: dict[str, Any] = dict(seed=seed, threads=threads) 1887 if cancel is not None: 1888 kwargs["cancel"] = cancel 1889 raw = self._native.refute(names, columns, suite, **kwargs) 1890 return _wrap_ate(raw, prepared=self) 1891 1892 1893__all__ = [ 1894 "AnalysisResult", 1895 "ConflictSummaryView", 1896 "EstimateView", 1897 "MediationView", 1898 "IdentificationView", 1899 "IdentifyResult", 1900 "NativeAnalysisResult", 1901 "PerformanceView", 1902 "PhysicalPlanView", 1903 "PlanView", 1904 "PosteriorView", 1905 "PredictiveCheckReport", 1906 "PreparedAnalysis", 1907 "PriorSensitivityReport", 1908 "TemporalAnalysisResult", 1909 "ValidationView", 1910 "analyze", 1911 "analyze_many", 1912 "identify", 1913]
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.
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.
92@dataclass(frozen=True) 93class EstimateView: 94 ate: float 95 se_analytic: float 96 se_bootstrap: float | None 97 estimator_id: str 98 method: str 99 overlap_ess: float | None = None 100 overlap_propensity_min: float | None = None 101 mediation: MediationView | None = None
76@dataclass(frozen=True) 77class IdentificationView: 78 status: str 79 method: str 80 adjustment_set: list[str] 81 assumption_count: int 82 derivation_step_count: int
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).
177@dataclass(frozen=True) 178class PerformanceView: 179 plan_id: str | None = None 180 modality: str | None = None 181 peak_memory_bytes: int | None = None 182 latency_mode: str | None = None 183 wall_time_ns: int | None = None 184 bootstrap_replicates_requested: int | None = None 185 bootstrap_replicates_ok: int | None = None 186 n_draws: int | None = None 187 cancelled: bool = False 188 early_stopped: bool = False 189 stage_timings: dict[str, int] | None = None
205@dataclass(frozen=True) 206class PhysicalPlanView: 207 """Physical-plan highlights from prepare (layouts / threads / kernels).""" 208 209 plan_id: str 210 estimated_peak_memory_bytes: int | None = None 211 workspace_bytes: int | None = None 212 batch_size: int | None = None 213 worker_threads: int = 0 214 expected_python_crossings: int = 0 215 deterministic_reductions: bool = True 216 kernels: str | None = None
Physical-plan highlights from prepare (layouts / threads / kernels).
192@dataclass(frozen=True) 193class PlanView: 194 """Logical-plan summary (semantics; inspect before/after estimate).""" 195 196 plan_id: str 197 modality: str | None = None 198 discovery_algorithm: str | None = None 199 graph_review_required: bool = False 200 identifier: str | None = None 201 estimator: str | None = None 202 validation_suite: str | None = None
Logical-plan summary (semantics; inspect before/after estimate).
113@dataclass(frozen=True) 114class PosteriorView: 115 effect_mean: float | None 116 effect_sd: float | None 117 q025: float | None 118 q975: float | None 119 n_draws: int | None 120 p_below_zero: float | None 121 backend: str | None 122 artifact: bytes | list[int] | None = None 123 unidentified_mass: float | None = None 124 envelope: EffectEnvelope | None = None 125 conflict: ConflictSummaryView | None = None
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.
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.
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.
167@dataclass(frozen=True) 168class ValidationView: 169 passed: bool 170 ran: bool 171 count: int 172 prior_predictive: PredictiveCheckReport | None = None 173 posterior_predictive: PredictiveCheckReport | None = None 174 prior_sensitivity: PriorSensitivityReport | None = None
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().