API reference¶
The shared objects live at the top level of ergodic, and the pillars under their own modules. Most people arrive here from search, so the table below maps a task to its entry point and the guide that teaches it.
| To do this | Start with | Guide |
|---|---|---|
| Build a causal graph | dag, admg, cpdag, pag |
Causal graphs |
| Turn a graph into an estimand | identify_effect |
Identification |
| Estimate an effect from data | estimate_effect |
Causal inference |
| Estimate per-unit effects (CATE) | cate |
Causal inference |
| Stress-test an estimate | refute_effect |
Checking an estimate |
| Learn a graph from data | discover, bootstrap |
Causal discovery |
| Measure a rollout with no graph | did, synthetic_control, interrupted_time_series |
Panel designs |
| Analyze a process from its log | process_map, case_table, discover_model |
Process intelligence |
| Simulate or ask counterfactuals | SCM |
Structural causal models |
Causal graphs¶
The mixed-graph object and its builders, separation, do-surgery, and interop.
ergodic.graph
¶
Causal graphs: one mixed-graph core with typed faces (DAG, ADMG, CPDAG, PAG).
Build graphs with the :func:dag, :func:admg, :func:cpdag, :func:pag, or
:func:mixed helpers. Edges read as glyph strings, so dag(["X -> Y", "Y -> Z"])
and pag(["X o-> Y", "Z o-> Y"]) do what they look like. Separation
(:func:m_separated), the orientation machinery (:func:meek_closure,
:func:fci_closure), the projections between the faces (:func:cpdag_of_dag,
:func:mag_of_dag, :func:pag_of_mag, :func:mag_of_pag), the class
toolkit (:func:possible_descendants, :func:visible_edges,
:func:dags_of_cpdag, :func:mags_of_pag), and the primitives the
identification algorithms read (:func:districts, :func:topological_order,
:func:latent_projection) live here too.
EdgeSpec = Edge | str | tuple[Any, ...]
module-attribute
¶
How a caller can name an edge: an Edge, a glyph string like "X o-> Y",
a 2-tuple (u, v) (read as u -> v), or a 4-tuple (u, u_mark, v, v_mark).
Edge
dataclass
¶
One edge between two distinct nodes, with a mark at each end.
Stored canonically with u < v (string order) so an undirected pair maps
to exactly one Edge. u_mark is the mark at u, v_mark at v.
Source code in src/ergodic/graph/_graph.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
directed_pair()
¶
Return (parent, child) if this edge is directed, else None.
Source code in src/ergodic/graph/_graph.py
96 97 98 99 100 101 102 | |
glyph()
¶
Render the edge, e.g. "X o-> Y".
Source code in src/ergodic/graph/_graph.py
104 105 106 107 108 | |
make(a, a_mark, b, b_mark)
staticmethod
¶
Build an edge between a and b, normalizing endpoint order.
Source code in src/ergodic/graph/_graph.py
55 56 57 58 59 60 61 62 | |
mark_at(node)
¶
Return the mark this edge carries at node.
Source code in src/ergodic/graph/_graph.py
64 65 66 67 68 69 70 | |
other(node)
¶
Return the endpoint that is not node.
Source code in src/ergodic/graph/_graph.py
72 73 74 75 76 77 78 | |
GraphKind
¶
Bases: Enum
The kind of a mixed graph, which fixes the allowed marks and rules.
Source code in src/ergodic/graph/_graph.py
27 28 29 30 31 32 33 34 35 | |
InvalidGraphError
¶
Bases: ValueError
A graph violates the rules of its declared :class:GraphKind.
Source code in src/ergodic/graph/_graph.py
38 39 | |
Mark
¶
Bases: Enum
A mark at one end of an edge.
A mixed graph edge has a mark at each end. TAIL (drawn -) and
ARROW (drawn >) give directed (->), bidirected (<->), and
undirected (--) edges. CIRCLE (drawn o) means the orientation is
unknown, the marks an FCI search leaves on a PAG.
Source code in src/ergodic/graph/_marks.py
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | |
char(*, left)
¶
Return the glyph for this mark on the left or right side of an edge.
Source code in src/ergodic/graph/_marks.py
22 23 24 25 26 27 28 | |
MixedGraph
dataclass
¶
An immutable mixed graph over named nodes.
Use the :func:dag, :func:admg, :func:cpdag, :func:pag, or
:func:mixed builders rather than constructing this directly.
Source code in src/ergodic/graph/_graph.py
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 | |
add_edge(spec)
¶
Add an edge on a free node pair; adding the edge already there is a no-op.
Raises ValueError when the pair already carries a different edge.
Call :meth:remove_edge first, or :meth:orient to redirect.
Source code in src/ergodic/graph/_graph.py
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 | |
as_kind(kind)
¶
Re-tag the graph as kind (an enum or its name) after checking it fits.
Source code in src/ergodic/graph/_graph.py
346 347 348 349 350 351 352 353 354 355 356 | |
do(targets)
¶
Graph surgery for an intervention: cut every arrowhead into targets.
Source code in src/ergodic/graph/_graph.py
500 501 502 503 504 | |
is_acyclic()
¶
Return True if the directed part of the graph has no cycle.
Source code in src/ergodic/graph/_graph.py
392 393 394 | |
is_admg()
¶
Directed or bidirected edges only, and no directed cycle. This check is complete.
Source code in src/ergodic/graph/_graph.py
400 401 402 403 | |
is_cpdag()
¶
Return True when the graph really is a CPDAG. This check is complete.
Three things must hold: every edge is directed or undirected, some DAG has exactly this skeleton and these v-structures (the Dor-Tarsi consistent extension exists), and the graph equals the CPDAG of that extension, so every compelled edge is directed and every reversible edge is left undirected.
Source code in src/ergodic/graph/_graph.py
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 | |
is_dag()
¶
Every edge directed and no directed cycle. This check is complete.
Source code in src/ergodic/graph/_graph.py
396 397 398 | |
is_mag()
¶
The ancestral rules of Richardson and Spirtes (2002).
Three edge types (directed, bidirected, undirected), no directed
cycle, no almost-directed cycle (a bidirected edge between ancestors),
and the endpoints of any undirected edge carry no arrowhead at all (no
parents and no spouses), which is how selection bias shows up in a
MAG. One gap: maximality (every non-adjacent pair separable) is not
checked here; the :meth:is_pag round trip is what exercises it.
Source code in src/ergodic/graph/_graph.py
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 | |
is_pag()
¶
Return True when the graph really is a complete PAG. This check is complete.
The round trip decides it, exactly as :meth:is_cpdag does for
CPDAGs: extract one member MAG (mag_of_pag), check it satisfies
the MAG rules, and require the canonical PAG of that member
(pag_of_mag) to be exactly this graph, so every invariant mark is
oriented and every varying mark is a circle. Selection bias is in
scope: undirected and circle-tail edges are read as Zhang's rules R5
to R7 leave them. One known gap rides along from :meth:is_mag:
maximality of the member is not checked.
Source code in src/ergodic/graph/_graph.py
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 | |
markov_blanket(x)
¶
Parents, children, and the other parents of children (DAG only).
Source code in src/ergodic/graph/_graph.py
280 281 282 283 284 285 286 287 288 | |
orient(parent, child)
¶
Return a graph with the edge between the two nodes set to parent -> child.
Source code in src/ergodic/graph/_graph.py
323 324 325 | |
set_mark(a, b, *, at, mark)
¶
Return a graph with the {a, b} edge's mark at endpoint at set to mark.
The finer-grained sibling of :meth:orient: one endpoint changes, the
other keeps its mark (how the FCI rules orient one mark at a time).
Raises KeyError when the pair carries no edge or at is not one
of its endpoints.
Source code in src/ergodic/graph/_graph.py
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | |
validate(kind=None)
¶
Check the graph against kind (default: its own), raising on failure.
Source code in src/ergodic/graph/_graph.py
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 | |
admg(edges=(), **kwargs)
¶
Build an acyclic directed mixed graph (directed and bidirected edges).
Source code in src/ergodic/graph/_graph.py
580 581 582 | |
consistent_extension(pdag)
¶
A DAG with exactly the PDAG's skeleton and v-structures, or None.
Dor and Tarsi (1992): repeatedly find a node x with no outgoing
directed edges whose undirected neighbours are adjacent to every other
neighbour of x, orient all undirected edges at x into x, and
retire x. When every node retires the oriented graph is the
extension; when no candidate exists none does.
Source code in src/ergodic/graph/_meek.py
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | |
cpdag(edges=(), **kwargs)
¶
Build a CPDAG (directed and undirected edges).
Source code in src/ergodic/graph/_graph.py
585 586 587 | |
cpdag_of_dag(dag)
¶
The CPDAG of a DAG: undirected skeleton, restored v-structures, Meek closure.
Every DAG in the Markov equivalence class of dag maps to the same
CPDAG, which keeps an edge directed exactly when every member orients it
the same way.
Source code in src/ergodic/graph/_meek.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | |
d_separated(graph, x, y, given=frozenset())
¶
d-separation, the DAG special case of :func:m_separated.
Source code in src/ergodic/graph/_separation.py
222 223 224 225 226 227 228 229 230 231 | |
dag(edges=(), **kwargs)
¶
Build a directed acyclic graph. Edges default to directed (u -> v).
Source code in src/ergodic/graph/_graph.py
575 576 577 | |
dags_of_cpdag(cpdag, *, max_members=None)
¶
Every DAG in the CPDAG's Markov equivalence class, in a deterministic order.
Branch on the first undirected edge, orient it each way, close under
Meek's rules, and recurse. Distinct branches fix that edge differently,
so no member appears twice and none is missed (the rules are sound, so
every member agrees with the closure of its own branch). Each leaf is
verified against the class definition, cpdag_of_dag(leaf) equal to
the input, so a branch that closes into a non-member dies quietly and
the answer is exact. max_members raises once the count would pass
it; there is no silent truncation.
Source code in src/ergodic/graph/_meek.py
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | |
district_of(graph, node)
¶
Return the district (c-component) containing node.
Source code in src/ergodic/graph/_primitives.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | |
districts(graph)
¶
The c-components: connected components of the bidirected (<->) subgraph.
Two observed nodes share a district when a <-> path joins them; a node
with no bidirected edge is its own singleton. Every node lands in exactly
one district and the districts partition graph.nodes. On a DAG (no
bidirected edges) every district is a singleton. Defined over the nodes as
they are: project latents first (:func:latent_projection) if the graph
still carries latent= designations.
Source code in src/ergodic/graph/_primitives.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | |
extendable(pdag)
¶
Return True when the PDAG admits a consistent extension.
Source code in src/ergodic/graph/_meek.py
229 230 231 | |
fci_closure(graph, *, discriminating)
¶
Close a partially oriented graph under Zhang's rules R1 to R10.
The input carries its v-structure arrowheads (and any knowledge-seeded
marks); circles elsewhere. Rules only ever turn a circle into a tail or
an arrowhead: a mark that is already definite never moves, so a seeded
mark that disagrees with a rule simply stays and the conflict surfaces at
the caller's validation. discriminating(d, a, b, c) answers R4's
question, whether b is a collider on the discriminating path; None
skips that path. Sweeps run in sorted order until a full pass changes
nothing, so the closure is deterministic. R5 to R7 are the selection
rules and never fire on a selection-free class; Zhang stages the rules
(R1 to R4, then R5 once, then R6 to R10) but every rule is individually
sound and marks only move circle to definite, so the joint fixpoint is
the same closure, which the round-trip tests hold executably. Returns
kind mixed.
Source code in src/ergodic/graph/_pag.py
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 | |
latent_projection(graph, keep=None)
¶
Project a DAG or ADMG with latents onto the observed margin, as an ADMG.
keep defaults to graph.nodes - graph.latent. The projected-out set
is H = nodes - keep. Over keep the result is an ADMG (kind=ADMG,
empty latent):
a -> biff a directed patha -> ... -> bhas every intermediate node inH.a <-> biff a path betweenaandbhas every intermediate node inHand a non-collider, with arrowheads into bothaandb.
Computed by iterative single-node elimination of each node in H, which
is equivalent. The defining contract is m-separation preservation: for
disjoint observed A, B, Z (none in H), the projection and
graph agree on m_separated(A, B, Z). This is the ADMG projection
the ID algorithm needs, distinct from the MAG ancestral projection
:func:mag_of_dag.
The result can carry a bow as two parallel edges on one pair (a -> b and
a <-> b), which the marks model otherwise forbids. This is correct for an
ADMG, and the ID engine reads it through parents, spouses,
districts, and m_separated, all of which see both edges. A projected
graph is an internal ID input, not a serializable one: to_dict,
to_adjacency, and to_networkx raise on a parallel-edge pair rather
than drop or mangle it.
Source code in src/ergodic/graph/_primitives.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | |
m_connected(graph, x, y, given=frozenset())
¶
Return True if some active path joins x and y given given.
Circle-free graphs (DAG, ADMG, MAG) use plain m-connection. A graph with circle marks (a PAG) uses definite m-connection, which on a valid complete PAG is the class-invariant answer. A CPDAG-kind graph gets the same class reading with its undirected edges recoded as circles; an undirected edge on any other kind is walked literally, as a tail at both ends.
Source code in src/ergodic/graph/_separation.py
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | |
m_separated(graph, x, y, given=frozenset())
¶
Return True if x and y are m-separated given given.
Source code in src/ergodic/graph/_separation.py
212 213 214 215 216 217 218 219 | |
mag_of_dag(graph)
¶
Project a DAG or ADMG with latent and selection nodes onto its MAG.
Two observed nodes are adjacent exactly when no observed subset separates
them given the selection nodes, decided by the one witness set that
suffices (Richardson and Spirtes 2002, Theorem 4.2): the observed
ancestors of either endpoint or of selection, plus the selection nodes
themselves. Marks read ancestry in the full graph: a tail at X on the
edge to Y means X is an ancestor of Y or of selection there,
an arrowhead means it is neither; two tails make an undirected edge,
which on a DAG means both endpoints are ancestors of selection. The
result preserves every m-separation over the observed margin conditional
on selection, which is the defining property of the projection.
Source code in src/ergodic/graph/_pag.py
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
mag_of_pag(p)
¶
Extract one member MAG from a PAG (Zhang 2008, arrowhead augmentation).
Every o-> edge becomes directed into its arrowhead, every -o
edge becomes directed out of its tail (the circle takes the arrowhead),
undirected and bidirected and directed edges stay as they are, and the
circle-circle component is oriented into a DAG that adds no unshielded
collider (the Dor-Tarsi extension of its undirected skeleton; for a
valid complete PAG that component is chordal, R5 guarantees it, so the
extension exists). An inextricable circle component or a result that
fails the MAG rules raises. Maximality of the member is not checked here
because is_mag does not check it; the is_pag round trip is what
guards it.
Source code in src/ergodic/graph/_pag.py
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 | |
mags_of_pag(p, *, max_members=None, selection=False)
¶
Every MAG in the PAG's equivalence class, in a deterministic order.
Each circle mark takes a tail or an arrowhead; an assignment survives
when it yields a valid MAG whose canonical PAG (:func:pag_of_mag) is
exactly p. With selection=False (the default) only the members
without undirected edges count: the reading every consumer that assumes
no selection bias quantifies over, identification among them. Pass
selection=True for the full Markov equivalence class, where a
circle-circle edge may also resolve to an undirected selection edge. A
PAG that itself carries selection marks (undirected or circle-tail
edges) has no selection-free members and raises under the default.
Exponential in the number of circle marks by design, which is fine at
the scale the library targets; max_members raises once the count
would pass it (no silent truncation). mag_of_pag(p) is always among
the members.
Source code in src/ergodic/graph/_pag.py
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 | |
meek_closure(pdag, *, forbidden=None, ambiguous=None)
¶
Apply Meek's rules R1 to R4 to a PDAG until fixpoint.
Each rule turns an undirected edge into a directed one whenever the
reverse orientation would create a cycle or a new v-structure in every
consistent extension. forbidden(u, v) returning True blocks the
orientation u -> v: the rule simply does not fire that way and the
edge stays undirected unless another rule orients it.
ambiguous lists unshielded triples (x, z, y), z the middle
node and the ends unordered, that the caller cannot certify as definite
non-colliders (PC's conservative and majority collider rules leave such
triples behind). A definite non-collider is exactly what R1 and R3
consume: R1 reads c -> a -- b with c and b nonadjacent as
compelling a -> b only because the collider c -> a <- b was ruled
out, and R3 rests on its two converging parents never colliding at the
undirected partner. Both rules skip a gated triple; R2 (a cycle rule)
and R4 run unchanged. Default None gates nothing.
The result is returned with kind mixed; re-face it with
:meth:MixedGraph.as_kind when more is known.
Source code in src/ergodic/graph/_meek.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
mixed(edges=(), *, nodes=(), latent=(), selection=(), kind=GraphKind.MIXED)
¶
Build a mixed graph of the given kind and check it satisfies that kind.
Source code in src/ergodic/graph/_graph.py
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 | |
pag(edges=(), **kwargs)
¶
Build a partial ancestral graph, validated as a complete PAG.
The marks must be exactly what an FCI run would leave: every invariant
mark oriented (closed under the orientation rules) and a circle wherever
the equivalence class disagrees. Build with :func:mixed instead when
you want circle marks without that guarantee.
Source code in src/ergodic/graph/_graph.py
590 591 592 593 594 595 596 597 598 | |
pag_of_mag(m)
¶
The canonical PAG of a MAG: the marks every Markov-equivalent MAG shares.
Same skeleton, every mark a circle; unshielded colliders copied from the
MAG (they are class invariants); then the rule closure, with R4's
discriminating-path question answered by the MAG's own marks. Zhang
(2008) proved the ten rules complete with latent confounders and
selection bias both, so the result carries exactly the invariant marks.
The input should be maximal; is_mag cannot check that, and a
non-maximal input gives a PAG for the wrong class.
Source code in src/ergodic/graph/_pag.py
633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 | |
possible_ancestors(graph, nodes)
¶
Nodes with a possibly directed path into nodes.
The mirror of :func:possible_descendants: w counts when some
member of the class can orient a path from w all the way into a
seed. Each seed is excluded from its own walk.
Source code in src/ergodic/graph/_pag.py
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | |
possible_descendants(graph, nodes)
¶
Nodes reachable from nodes along a possibly directed path.
A path is possibly directed when no edge on it carries an arrowhead at
the endpoint nearer the start, so some member of the graph's class can
point every edge away from the start. On a DAG or ADMG this is plain
descendants. Each seed is excluded from its own walk, mirroring
:meth:MixedGraph.descendants.
Source code in src/ergodic/graph/_pag.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | |
topological_order(graph)
¶
A topological order over the directed edges, tie-broken by node name.
For every directed edge a -> b, a precedes b. Bidirected edges
impose no order. The name tie-break makes the order stable across runs, so
estimands that read it render reproducibly. Raises on a graph whose
directed part has a cycle.
Source code in src/ergodic/graph/_primitives.py
77 78 79 80 81 82 83 84 85 86 87 88 | |
visible_edges(graph)
¶
The visible directed edges, as (tail, head) pairs (Zhang 2008).
A -> B is visible when some C not adjacent to B has an edge
into A, or reaches A through a collider path into A whose
every interior node is a parent of B. Visibility certifies the edge
carries no hidden confounding in any represented world, which is what
adjustment amenability on MAGs and PAGs consumes. The marks are read as
given (arrowheads must be definite); on a DAG or CPDAG every directed
edge is visible by causal sufficiency, and callers there should not need
this function.
Source code in src/ergodic/graph/_pag.py
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | |
Domain knowledge¶
Hard prior constraints (required and forbidden edges, tiers, roots, sinks) that discovery and inference read.
ergodic.knowledge
¶
Domain knowledge: prior constraints on causal structure.
DomainKnowledge records what a user knows before looking at the data, the
edges that must or must not exist, the temporal order of variables, which
variables are roots or sinks, and which pairs share a hidden confounder. A
skeleton restriction (restrict_to) caps which pairs may be adjacent at all.
It is an immutable value object built up with small methods, and two knowledge
objects combine with & (more knowledge means fewer admissible graphs).
Hard constraints are the focus for now. The energy and log_prior hooks
are where soft, probabilistic knowledge will attach once the Bayesian work
starts; they raise for the moment so nothing depends on a half-built scoring rule.
DomainKnowledge
dataclass
¶
Prior constraints on a causal graph over named variables.
Source code in src/ergodic/knowledge.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 | |
__and__(other)
¶
Combine two knowledge objects by taking the union of their constraints.
Skeleton allow-lists intersect: an adjacency survives only when both
sides allow it, and None (unrestricted) defers to the other side.
Source code in src/ergodic/knowledge.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
apply_to(graph)
¶
Enforce the hard constraints on graph: drop forbidden, add required edges.
A pair forbidden in both directions loses its edge whatever the marks; a single-direction ban only drops the matching directed edge. Under a skeleton restriction, every edge on a pair outside the allow-list is dropped too. Edges left undirected or with a circle are oriented when only one direction stays permitted. The result is returned as a new graph.
Source code in src/ergodic/knowledge.py
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 | |
conflicts()
¶
Return a list of internal contradictions, empty when consistent.
Source code in src/ergodic/knowledge.py
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
confound(a, b)
¶
Record that a and b share a hidden common cause.
Source code in src/ergodic/knowledge.py
113 114 115 116 117 118 119 120 121 | |
edge_mask(variables=None)
¶
Return a boolean matrix of permitted directed edges and its variable order.
mask[i, j] is True when an edge from variable i to variable
j is allowed. The diagonal is False, and a skeleton restriction
zeroes both directions of every pair outside its allow-list. Useful as
a search mask for score-based and continuous-optimization discovery.
Source code in src/ergodic/knowledge.py
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 | |
energy(graph)
¶
Penalty for graph under soft knowledge. Planned for the Bayesian phase.
Source code in src/ergodic/knowledge.py
418 419 420 421 422 423 | |
forbid(cause, effect)
¶
Forbid the directed edge cause -> effect (the reverse stays allowed).
Source code in src/ergodic/knowledge.py
64 65 66 67 68 69 70 | |
forbid_adjacency(a, b)
¶
Forbid any edge between two variables, in either direction.
Source code in src/ergodic/knowledge.py
72 73 74 75 76 77 78 | |
forbid_between(group_a, group_b)
¶
Forbid every directed edge from group_a into group_b.
Source code in src/ergodic/knowledge.py
123 124 125 126 127 128 129 130 | |
is_consistent(graph)
¶
Return True if graph satisfies every constraint.
Source code in src/ergodic/knowledge.py
355 356 357 | |
log_prior(graph)
¶
Log prior of graph under soft knowledge. Planned for the Bayesian phase.
Source code in src/ergodic/knowledge.py
425 426 427 428 429 430 | |
order(before, after)
¶
Record that before precedes after in time, so after cannot cause it.
Source code in src/ergodic/knowledge.py
80 81 82 83 84 85 86 | |
permits_edge(cause, effect)
¶
Return True if a directed edge cause -> effect is allowed.
Precedence is read transitively: with A before B and B
before C, an edge C -> A is not permitted. Under a skeleton
restriction, a pair outside the allow-list fails in both directions.
Source code in src/ergodic/knowledge.py
286 287 288 289 290 291 292 293 | |
precedence()
¶
Every (before, after) pair, with the order read transitively.
The projection discovery reads to orient: (a, b) here means b
cannot be an ancestor of a, which in a latent-variable search
(FCI) pins an arrowhead at b on any surviving adjacency.
Source code in src/ergodic/knowledge.py
298 299 300 301 302 303 304 305 | |
require(cause, effect)
¶
Require a directed edge cause -> effect.
Source code in src/ergodic/knowledge.py
56 57 58 59 60 61 62 | |
restrict_to(skeleton)
¶
Allow adjacencies only on the pairs of skeleton.
skeleton is a :class:~ergodic.graph.MixedGraph (its edges name
the pairs, whatever the marks) or an iterable of (u, v) pairs. A
pair outside the allow-list is banned in every form: directed either
way, bidirected, or undirected. Restricting an already restricted
object keeps the intersection, so a second call only narrows the
support. An empty iterable allows no adjacencies at all.
Source code in src/ergodic/knowledge.py
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |
root(*names)
¶
Mark variables as roots: no incoming directed edges.
Source code in src/ergodic/knowledge.py
101 102 103 104 105 | |
sink(*names)
¶
Mark variables as sinks: no outgoing directed edges.
Source code in src/ergodic/knowledge.py
107 108 109 110 111 | |
validate()
¶
Raise :class:InconsistentKnowledgeError if the knowledge contradicts itself.
Source code in src/ergodic/knowledge.py
247 248 249 250 251 | |
violations(graph)
¶
Return the ways graph breaks this knowledge, empty when consistent.
Source code in src/ergodic/knowledge.py
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 | |
with_tiers(tiers)
¶
Add a tiered order: no variable in a later tier causes one in an earlier tier.
Source code in src/ergodic/knowledge.py
88 89 90 91 92 93 94 95 96 97 98 99 | |
InconsistentKnowledgeError
¶
Bases: ValueError
Domain knowledge contradicts itself (for example a required edge cycle).
Source code in src/ergodic/knowledge.py
30 31 | |
Data¶
The typed dataset family across five shapes, and its accessors.
ergodic.data
¶
Typed datasets: the data objects discovery and inference read.
Each shape embeds a column :class:Schema (continuous, categorical, ordinal,
datetime, or excluded) and shares the accessors on :class:Dataset. The shapes
are tabular, time-series, panel, and hierarchical time-series; the event log
follows.
Build a dataset with :func:tabular, :func:timeseries, :func:panel, or
:func:hts, each of which takes a pandas or polars dataframe, a dict of columns,
or a numpy array.
ColumnType
dataclass
¶
The type of one column: its kind, and the categories when it is discrete.
Source code in src/ergodic/data/_schema.py
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
is_discrete
property
¶
Categorical or ordinal: backed by a finite set of categories.
Dataset
dataclass
¶
A typed column store. Built through the shape factories, not directly.
Source code in src/ergodic/data/_dataset.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | |
index_columns
property
¶
Columns that index the rows (time, entity, case), not modeled variables.
variables
property
¶
Column names the algorithms see: not the excluded ones, not the index.
alignment(graph)
¶
Where the variables and the graph's nodes disagree, empty when they match.
Source code in src/ergodic/data/_dataset.py
204 205 206 207 208 209 210 | |
aligns_with(graph)
¶
True when the variables are exactly the graph's nodes.
Source code in src/ergodic/data/_dataset.py
212 213 214 | |
codes(name)
¶
Integer category codes for a categorical or ordinal column, -1 for missing.
A non-missing value outside the column's categories raises a ValueError.
Source code in src/ergodic/data/_dataset.py
85 86 87 88 89 90 91 92 93 | |
column(name)
¶
A read-only copy of one column's raw values.
Source code in src/ergodic/data/_dataset.py
77 78 79 80 81 82 83 | |
dropna()
¶
Drop every row with a missing value in any column.
Source code in src/ergodic/data/_dataset.py
140 141 142 143 144 145 146 | |
exclude(*names)
¶
A new dataset with the named columns marked excluded.
Source code in src/ergodic/data/_dataset.py
195 196 197 198 199 200 | |
matrix(names=None)
¶
A float matrix of the named columns. Discrete columns become their codes, and a missing value (an NA category or NaT) becomes NaN.
Source code in src/ergodic/data/_dataset.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | |
missing()
¶
Missing-value count per column.
Source code in src/ergodic/data/_dataset.py
132 133 134 | |
select(names)
¶
A new dataset with the named columns, plus any index columns, in order.
Source code in src/ergodic/data/_dataset.py
166 167 168 169 170 171 172 173 174 175 176 177 | |
take(rows)
¶
A new dataset with the rows at the given positions, in the given order.
Positions may repeat (a bootstrap resample) and may be negative (Python indexing). Row identity is all this knows about; shapes with row semantics (time series, panel) keep their index columns but it is the caller's job to resample in a way their structure survives.
Source code in src/ergodic/data/_dataset.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |
with_type(name, kind, categories=None)
¶
A new dataset with name retyped, categories read from data when needed.
Source code in src/ergodic/data/_dataset.py
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
EventLog
dataclass
¶
Bases: Dataset
Events keyed by case, activity, and timestamp; one row per event.
Source code in src/ergodic/data/_eventlog.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
directly_follows()
¶
Counts of each (a, b) where b immediately follows a in a case.
Source code in src/ergodic/data/_eventlog.py
74 75 76 77 78 79 80 | |
traces()
¶
The ordered activity sequence per case.
Source code in src/ergodic/data/_eventlog.py
61 62 63 64 65 66 67 68 | |
variants()
¶
How many cases follow each distinct activity sequence.
Source code in src/ergodic/data/_eventlog.py
70 71 72 | |
HierarchicalTSData
dataclass
¶
Bases: TimeSeriesData
A time-series whose columns form an aggregation hierarchy.
Source code in src/ergodic/data/_hts.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
aggregate(node)
¶
Build a node's series from its direct children, recursively.
A child contributes its own column when it has one, else its aggregate,
and the parent is the sum or mean of those child series. This is the same
definition :meth:coherence checks, so the two cannot disagree.
Source code in src/ergodic/data/_hts.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | |
coherence(rtol=1e-05, atol=1e-08)
¶
Where a present parent column does not match :meth:aggregate of its children.
Source code in src/ergodic/data/_hts.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
is_coherent(rtol=1e-05, atol=1e-08)
¶
True when every present parent equals the aggregate of its children.
Source code in src/ergodic/data/_hts.py
76 77 78 | |
Hierarchy
dataclass
¶
A tree of named series, with edges running parent to child, and an aggregation rule.
Source code in src/ergodic/data/_hierarchy.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
internal_nodes
property
¶
Nodes with children: the aggregates.
leaves
property
¶
Nodes with no children: the base series.
roots
property
¶
Nodes with no parent.
leaf_descendants(node)
¶
The base series that aggregate up to node (node itself when it is a leaf).
Source code in src/ergodic/data/_hierarchy.py
102 103 104 105 106 107 108 109 | |
levels()
¶
Depth of each node, with roots at 0.
Source code in src/ergodic/data/_hierarchy.py
111 112 113 114 115 116 117 118 119 120 | |
tree(adjacency, aggregation='sum')
classmethod
¶
Build from a parent-to-children mapping, for example {"total": ["a", "b"]}.
Source code in src/ergodic/data/_hierarchy.py
53 54 55 56 57 | |
Ordinal
dataclass
¶
Mark a column ordinal, listing its categories in increasing order.
Use it in a type override: types={"stage": Ordinal(["I", "II", "III"])}.
Source code in src/ergodic/data/_schema.py
50 51 52 53 54 55 56 57 58 59 60 | |
PanelData
dataclass
¶
Bases: Dataset
Entities observed over time, keyed by a unique (entity, time) pair.
Source code in src/ergodic/data/_panel.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | |
is_balanced
property
¶
True when every entity is seen in every period (a full grid).
time_invariant()
¶
Variables whose value never changes within an entity.
Source code in src/ergodic/data/_panel.py
64 65 66 67 68 69 70 71 72 | |
Schema
dataclass
¶
An ordered set of column types, queryable by name and by kind.
Source code in src/ergodic/data/_schema.py
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |
discrete
property
¶
Categorical and ordinal columns together.
variables
property
¶
Every column the algorithms see: all but the excluded ones.
with_type(name, kind, categories=None)
¶
Return a new schema with name retyped. Marks the column user-set.
Source code in src/ergodic/data/_schema.py
172 173 174 175 176 177 178 179 180 | |
SchemaInferenceWarning
¶
Bases: UserWarning
A column type was guessed from ambiguous data (for example integer levels).
Source code in src/ergodic/data/_schema.py
33 34 | |
TabularData
dataclass
¶
Bases: Dataset
Cross-sectional data: rows are independent units, with no index column.
Source code in src/ergodic/data/_tabular.py
25 26 27 | |
TimeSeriesData
dataclass
¶
Bases: Dataset
Series ordered by a time column, with an optional sampling frequency.
Source code in src/ergodic/data/_timeseries.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
times
property
¶
The time index, in ascending order.
VariableType
¶
Bases: Enum
How a column is measured, which decides the methods that apply to it.
Source code in src/ergodic/data/_schema.py
37 38 39 40 41 42 43 44 45 46 47 | |
event_log(data, *, case, activity, timestamp, columns=None, types=None, exclude=None, categorical_threshold=10)
¶
Build an :class:EventLog, grouped by case and ordered by timestamp.
Source code in src/ergodic/data/_eventlog.py
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
hts(data, *, time, hierarchy, freq=None, columns=None, types=None, exclude=None, categorical_threshold=10)
¶
Build a :class:HierarchicalTSData from series columns and a hierarchy.
Source code in src/ergodic/data/_hts.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | |
infer_schema(store, *, n_rows, declared=None, overrides=None, exclude=None, categorical_threshold=10)
¶
Build a schema from columns: user overrides beat source-declared beat inferred.
Source code in src/ergodic/data/_schema.py
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | |
panel(data, *, entity, time, freq=None, columns=None, types=None, exclude=None, categorical_threshold=10)
¶
Build a :class:PanelData, keyed and sorted by (entity, time).
Source code in src/ergodic/data/_panel.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
tabular(data, *, columns=None, types=None, exclude=None, categorical_threshold=10)
¶
Build a :class:TabularData from a dataframe, a dict of columns, or an array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
Any
|
A pandas or polars dataframe, a mapping of name to column, or a
2-D numpy array (name the columns with |
required |
columns
|
Sequence[str] | None
|
Column names, used when |
None
|
types
|
Mapping[str, VariableType | str | Ordinal] | None
|
Per-column type overrides, for example
|
None
|
exclude
|
Iterable[str] | None
|
Columns to mark excluded, so they are kept but not treated as variables (identifiers, free text). |
None
|
categorical_threshold
|
int
|
Integer columns with at most this many distinct values are inferred categorical, with a warning. |
10
|
Source code in src/ergodic/data/_tabular.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
timeseries(data, *, time, freq=None, columns=None, types=None, exclude=None, categorical_threshold=10)
¶
Build a :class:TimeSeriesData, sorting rows by the time column.
The time column is typed from its data (datetime, else continuous) unless you override it, so an integer step index is not mistaken for a category.
Source code in src/ergodic/data/_timeseries.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
Identification¶
Graph to estimand: adjustment, front-door, instruments, the general ID algorithm, and mediation.
ergodic.identification
¶
Identification: turn a causal graph into an estimand.
Given a graph, a treatment, and an outcome, the question is whether the interventional distribution P(Y | do(X)) can be written in terms of the observational distribution, and if so, how. The work is graph-only and deterministic: no data, no statistics, no estimator choices yet.
The named criteria (adjustment, front-door, instruments) live in
:mod:._criteria; the :class:Identification record in :mod:._record.
:func:identify_effect tries them in order, falling through to the general
do-calculus ID algorithm on a single graph.
The general engine is exposed too: :func:identify_distribution runs ID (or
IDC) for any query and returns an :class:IDResult carrying the typed estimand
:class:Functional (or the :class:Hedge witness when not identifiable).
:func:ida covers an equivalence class by enumerating members.
Functional
¶
The sealed base of the estimand algebra. Use the frozen subclasses below.
Three operations are common to every node: :meth:render (the readable
estimand string), :meth:free_vars (the variable scope, used by recognition
and the plug-in evaluator), and :meth:simplify (conservative rewrites
only). Subclasses are frozen value objects, so two structurally equal
functionals are equal and hash alike.
Source code in src/ergodic/identification/_functional.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | |
free_vars()
¶
The variable scope: heads and givens, minus bound marginal indices.
Source code in src/ergodic/identification/_functional.py
43 44 45 | |
render()
¶
The readable estimand string, e.g. sum_z P(Y | X, z) P(z).
Source code in src/ergodic/identification/_functional.py
39 40 41 | |
simplify()
¶
A conservatively simplified functional (safe rewrites only).
Flatten nested products, drop a marginal over an empty index, drop a factor that is identically one. No cancellations, no clever algebra: two runs render the same estimand.
Source code in src/ergodic/identification/_functional.py
47 48 49 50 51 52 53 54 | |
Hedge
dataclass
¶
Bases: Exception
The structural witness that an effect is not identifiable (a C-forest pair).
ID fails at a subproblem whose whole graph is a single c-component over
nodes and whose treatment-free part is the district district. That
pair is a hedge in the sense of Shpitser-Pearl: it is the obstruction that
proves P(y | do(x)) is not a function of the observational distribution.
The SCM bounds layer reads this object to decide where to return bounds
instead of a point.
The witness is the (nodes, district) summary of the failing subproblem,
enough to mark non-identifiability and to seed the bounds. The explicit
minimal C-forest pair (the rooted forests of Shpitser-Pearl) is not extracted
yet; that is a refinement for later.
Source code in src/ergodic/identification/_id.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
render()
¶
A human-readable reason.
Source code in src/ergodic/identification/_id.py
67 68 69 70 71 72 | |
IDA
dataclass
¶
Per-member identification across an equivalence class.
members is the tuple of per-member :class:Identification results, in
the enumerator's deterministic order. For a CPDAG that is one entry per DAG
member; for a PAG or MAG handled through the class-level adjustment criterion
it is a single entry standing for the whole class (the criterion's answer
holds in every member). class_kind records the face that was enumerated.
The summary properties read the multiset: how many members identify, the distinct strategies and estimands seen, and whether the class agrees.
Source code in src/ergodic/identification/_ida.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
agree
property
¶
True when every member identifies and shares one estimand.
The classic IDA reading: a single, member-invariant answer. False
when some member fails or the members disagree about the estimand.
all_identifiable
property
¶
True when every member identifies.
estimands
property
¶
The distinct estimand strings across the identifiable members.
n_identifiable
property
¶
How many members have an identifiable effect.
n_members
property
¶
The number of members examined.
strategies
property
¶
The distinct strategies seen across members (e.g. {"adjustment", "id"}).
IDResult
dataclass
¶
The outcome of running ID or IDC on a single graph.
identifiable is the verdict; functional is the estimand when it
holds (None otherwise); hedge is the witness when it fails (None
otherwise). query records the (outcome, do, given) that was asked.
Source code in src/ergodic/identification/_id.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
Identification
dataclass
¶
The result of trying to identify an effect from a graph.
The named strategies ("null", "adjustment", "frontdoor",
"iv") carry the set or instruments their estimator reads and a string
estimand. When only the general do-calculus ID algorithm identifies an
effect (outside the three named criteria, the napkin and Verma graphs and
their kin), strategy is "id" and functional carries the typed
estimand, with estimand set to functional.render(). A
not-identifiable result built by the ID fallthrough carries the hedge
witness. Existing strategies set neither, and estimand stays a string,
so the field is a pure extension.
Source code in src/ergodic/identification/_record.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | |
Marginal
dataclass
¶
Bases: Functional
Sum index out of body (a marginalization).
Source code in src/ergodic/identification/_functional.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |
MediationID
dataclass
¶
The result of trying to identify a mediation decomposition from a graph.
identifiable is the verdict for the natural direct and indirect effects:
True when a covariate set covariates makes the mediation formula
valid, False when none exists (the cross-world / post-treatment-confounder
failure). nde_estimand and nie_estimand render the mediation formula
with that set; they are empty strings when not identifiable.
The controlled direct effect is tracked separately, since it is a plain
interventional contrast (no cross-world assumption): cde_identifiable is
its verdict and cde_estimand its formula, decided through the general ID
algorithm on P(Y | do(X), do(M)). CDE can be identified when NDE/NIE are
not, and vice versa.
assumptions states, in words, the (untestable) claims the decomposition
rests on, the same honesty the :class:~ergodic.inference.Design records carry
for the quasi-experimental designs.
Source code in src/ergodic/identification/_mediation.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
Prob
dataclass
¶
Bases: Functional
An atomic term P(head | given) (a marginal when given is empty).
Source code in src/ergodic/identification/_functional.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
Product
dataclass
¶
Bases: Functional
A product of factors.
Source code in src/ergodic/identification/_functional.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
Ratio
dataclass
¶
Bases: Functional
A ratio num / den (the IDC normalization P_x(y, z) / P_x(z)).
Source code in src/ergodic/identification/_functional.py
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
adjustment_set(graph, treatment, outcome)
¶
Return the canonical valid adjustment set, or None if none exists.
The canonical set is the ancestors of treatment and outcome (possible ancestors, on a class face), minus the forbidden nodes and the latent nodes. If any valid adjustment set over the observed nodes exists, this one is valid; on a class, validity means valid in every member.
Source code in src/ergodic/identification/_criteria.py
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 | |
frontdoor_set(graph, treatment, outcome)
¶
Return a front-door mediator set, or None if none is found.
Tries the full set of observed nodes between treatment and outcome first, then single mediators. This is a search over likely candidates, not every subset.
Source code in src/ergodic/identification/_criteria.py
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | |
ida(graph, treatment, outcome, *, max_members=_DEFAULT_MAX_MEMBERS)
¶
Identify treatment -> outcome across every member of an equivalence class.
On a CPDAG, enumerate the DAG members (:func:dags_of_cpdag) and run
:func:identify_effect per member, returning the multiset as an :class:IDA.
On a PAG or MAG, return the class-level generalized-adjustment answer where it
applies (a single member-invariant result), and raise when the case needs
general per-member ID (deferred: it needs a DAG realization of each MAG
member). A single graph (DAG or ADMG) raises with a pointer to
:func:identify_effect.
Source code in src/ergodic/identification/_ida.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
identify_distribution(graph, outcome, do, given=frozenset())
¶
Run the general do-calculus ID (or IDC) algorithm on a single graph.
Decides whether P(outcome | do(do), given) is identifiable from the
observational distribution and, when it is, returns the estimand as a typed
functional inside an :class:~ergodic.identification._id.IDResult. With
given empty this is ID; with given nonempty it is the conditional
algorithm IDC. The graph may be a DAG or an ADMG, and may carry latent=
designations or bows (it is latent-projected to an ADMG first).
A CPDAG, MAG, or PAG raises: ID is defined on a single graph, and the class
is covered by :func:~ergodic.identification.ida, which enumerates members.
A mixed-face graph raises, like the rest of identification.
Source code in src/ergodic/identification/_distribution.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | |
identify_effect(graph, treatment, outcome)
¶
Try adjustment, then the front-door, then instruments, and report the result.
Before any strategy runs, the trivial case is settled: when no directed
path runs from the treatment to the outcome, the intervention cannot move
the outcome, so P(y | do(x)) = P(y) and the strategy is "null".
On a class face (CPDAG, MAG, PAG) only the null and adjustment strategies apply, and a success holds in every graph the class represents: the null when no member has a causal path, adjustment through the generalized criterion. The front-door and instruments need a single graph, so a class that adjustment cannot identify reports not-identifiable and says whether amenability failed or no uniform set exists.
Source code in src/ergodic/identification/_criteria.py
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 | |
identify_mediation(graph, treatment, outcome, mediators)
¶
Identify the mediation decomposition of treatment -> outcome through mediators.
Searches for a covariate set W of non-descendants of the treatment under
which the natural direct and indirect effects are identified by the mediation
formula (Pearl, 2001): W is a valid adjustment set for X -> Y and for
X -> M, and W u {X} is a valid adjustment set for M -> Y. When such
a W exists, NDE and NIE are identifiable and their estimands render in the
mediation-formula style; when none exists, they are not identifiable, the
canonical case being a mediator-outcome confounder that is itself affected by
the treatment (a cross-world obstruction).
The controlled direct effect is identified separately, through the general ID
algorithm on P(Y | do(X), do(M)): a plain interventional contrast that
needs no cross-world assumption, and may identify when NDE/NIE do not.
Single graphs only (a DAG or ADMG). A CPDAG, MAG, or PAG raises (extract a member or enumerate the class).
Source code in src/ergodic/identification/_mediation.py
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 | |
instruments(graph, treatment, outcome)
¶
Find conditional instruments for treatment -> outcome.
Returns (instrument, conditioning set) pairs. Candidates and conditioning
nodes are observed non-descendants of the treatment and the outcome; the
search tries the empty set and single nodes as conditioning sets.
Source code in src/ergodic/identification/_criteria.py
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 | |
is_frontdoor_set(graph, treatment, outcome, mediators)
¶
Return True if mediators satisfies the front-door criterion.
A mediator set holding a latent node is never valid, since the front-door formula conditions on the mediators.
Source code in src/ergodic/identification/_criteria.py
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | |
is_instrument(graph, instrument, treatment, outcome, given=frozenset())
¶
Return True if instrument is a conditional instrument given given.
The instrument and the conditioning set are observed non-descendants of the treatment and the outcome, the instrument is associated with the treatment, and the instrument reaches the outcome only through the treatment (it separates from the outcome once the treatment's outgoing edges are cut).
Descendants of the treatment are rejected outright: cutting the treatment's outgoing edges would sever the very edge that makes them endogenous, so the exclusion check alone would pass them.
Source code in src/ergodic/identification/_criteria.py
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 | |
is_valid_adjustment_set(graph, treatment, outcome, adjustment)
¶
Return True if adjustment is a valid adjustment set for the effect.
On a DAG or ADMG, the complete adjustment criterion: the set avoids the forbidden nodes (descendants of the causal-path nodes) and blocks every proper non-causal path from treatment to outcome. A set holding a latent node is never valid, since a latent node cannot be conditioned on.
On a CPDAG, MAG, or PAG, the generalized criterion (Perkovic et al.,
2018): the class must be amenable, the set avoids the possible
descendants of the possibly-causal-path nodes, and it blocks every
proper definite-status non-causal path. True then means the set is
valid in every graph the class represents.
Source code in src/ergodic/identification/_criteria.py
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 | |
minimal_adjustment_set(graph, treatment, outcome)
¶
Return one minimal valid adjustment set, or None if none exists.
Source code in src/ergodic/identification/_criteria.py
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | |
Causal discovery¶
Structure learning: the algorithms, the CI tests and scores they consume, and the discover facade.
ergodic.discovery
¶
Causal discovery: learn causal structure from data.
Two ingredients drive the searches. A conditional-independence test
(:class:CITest) is a class constructed with data, which makes it the
stateful oracle the constraint algorithms query: :class:Linear (partial
correlation), :class:Discrete (G squared, natively categorical),
:class:Mixed (degenerate Gaussian for mixed columns), :class:GCM (any
regression :class:~ergodic.inference.Learner becomes a test), and
:class:DSeparationOracle as ground truth. A decomposable :class:Score
answers local(node, parents) for the score-based searches: :class:BIC
(Gaussian), :class:DiscreteBIC, :class:BDeu, and :class:MixedBIC
(degenerate Gaussian). Algorithms are thin consumers: :class:PC wraps a
test and returns a CPDAG, :class:FCI wraps a test and returns a PAG (latent
confounders allowed), :class:MMPC wraps a test and returns an undirected
skeleton screen, :class:GES wraps a score and returns a CPDAG,
:class:AStar wraps a score and returns the exact optimum, and
:class:HillClimb is the fast greedy baseline. discover is the one-line
facade, and :func:bootstrap reruns any method on resamples and returns edge
frequencies (:class:BootstrapResult), the first soft output. Background
knowledge (including a skeleton restriction through restrict_to) enters
through :class:ergodic.DomainKnowledge.
Time series and panels get their own methods: :class:PCMCI reads a
:class:~ergodic.data.TimeSeriesData or :class:~ergodic.data.PanelData
through a lagged design matrix (:func:lagged_tabular) and returns a window
graph over nodes like "X[t-1]" (:class:TimeSeriesDiscoveryResult), and
:func:granger is the predictive baseline, a pair-level table on purpose
rather than a graph.
AStar
¶
Exact structure search: the highest-BIC DAG, found with A*.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
score
|
Score | str
|
A :class: |
'auto'
|
max_parents
|
int | None
|
An in-degree cap; |
None
|
node_limit
|
int
|
Refuse problems with more variables than this; exact search is exponential in the node count. |
20
|
Source code in src/ergodic/discovery/algorithms/_astar.py
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | |
fit(data=None, knowledge=None)
¶
Search for a highest-scoring DAG and return it with its provenance.
Source code in src/ergodic/discovery/algorithms/_astar.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | |
BDeu
¶
Bases: Score
The Bayesian Dirichlet equivalent uniform score, decomposable.
Every column must be discrete (categorical, ordinal, or binary): the
score runs on integer codes, so binding to data with a continuous column
raises TypeError. Score-equivalent for any fixed ess, but the
value of ess shapes the result; see the module docstring.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
TabularData | None
|
Bind at construction, or later through |
None
|
ess
|
float
|
The equivalent sample size, the total prior mass spread over
each node's table. Must be positive. Larger values smooth the
cell estimates and favor denser graphs; |
10.0
|
Source code in src/ergodic/discovery/scores/_bdeu.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
ess
property
¶
The equivalent sample size of the Dirichlet prior.
local_from_codes(y, y_card, zs, z_cards)
¶
The Dirichlet log marginal likelihood from integer codes.
Source code in src/ergodic/discovery/scores/_bdeu.py
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
BIC
¶
Bases: Score
The Gaussian BIC, decomposable and score-equivalent.
Nodes must be single numeric columns (continuous, datetime, or binary);
scoring a wider categorical block raises TypeError lazily, at the
first query against that node. Categorical parents are fine: their
one-hot columns enter the regression and each column counts toward k.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
TabularData | None
|
Bind at construction, or later through |
None
|
penalty
|
float
|
A multiplier on the complexity term |
1.0
|
Source code in src/ergodic/discovery/scores/_bic.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | |
penalty
property
¶
The multiplier on the complexity term.
local_from_arrays(y, X)
¶
The same local by direct least squares on (n, 1) y and (n, k) X.
Agrees with the covariance path to numerical precision; the tests hold
the two to 1e-8.
Source code in src/ergodic/discovery/scores/_bic.py
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |
BOSS
¶
Best order score search over a decomposable score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
score
|
Score | str
|
A :class: |
'auto'
|
use_bes
|
bool
|
Finish with the backward equivalence step (GES's delete phase) on the CPDAG of the projected DAG. The paper's correctness proposition needs it, so it defaults on; the paper's experiments ran without it. |
True
|
restarts
|
int
|
Extra runs from shuffled starting permutations, on top of the sorted start. The best final score across starts wins, with the earlier start keeping ties. |
0
|
seed
|
int | None
|
The seed for the restart shuffles. |
None
|
fit(data, knowledge) returns a :class:DiscoveryResult whose graph
carries the CPDAG face when the final PDAG is a valid CPDAG and the
mixed face otherwise (forced knowledge orientations can leave the
class; diagnostics["cpdag_valid"] says which happened).
Source code in src/ergodic/discovery/algorithms/_boss.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | |
fit(data=None, knowledge=None)
¶
Sweep permutations to convergence and return the class found.
Source code in src/ergodic/discovery/algorithms/_boss.py
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
BootstrapResult
dataclass
¶
Edge frequencies from rerunning one discovery method on bootstrap resamples.
edges maps an unordered node pair to the fraction of successful runs
in which the pair was adjacent, whatever the marks. marks maps a
canonical edge glyph (:meth:~ergodic.graph.Edge.glyph, for example
"X --> Y" or "X <-> Y") to its frequency, so one pair can spread
its mass over several glyphs; the glyph frequencies for a pair sum to its
adjacency frequency. Both mappings hold only pairs and glyphs that
appeared at least once, with keys in sorted order. Frequencies are over
the n_runs successful replicates, not over what was attempted. A
temporal method keys them on the window nodes, "X[t-1] --> Y[t]".
Source code in src/ergodic/discovery/_bootstrap.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
frequency(a, b)
¶
The adjacency frequency of the pair, 0.0 when it never appeared.
Source code in src/ergodic/discovery/_bootstrap.py
64 65 66 | |
summary(threshold=0.5)
¶
The stability graph: pairs at or above threshold, one edge each.
A qualifying pair gets its modal glyph, the orientation it wore most
often (ties broken by the sorted glyph string, so the result is
deterministic). The graph keeps the mixed kind: a vote over graphs
carries no equivalence-class guarantee, even when every replicate was
a CPDAG. Nodes are all the data's variables, so a variable that never
earned a stable edge shows up isolated. threshold must be in
(0, 1].
Source code in src/ergodic/discovery/_bootstrap.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
CIResult
dataclass
¶
One conditional-independence query: the statistic, its p-value, and dof.
Source code in src/ergodic/discovery/_ci.py
41 42 43 44 45 46 47 | |
CITest
¶
Base for conditional-independence tests; see the module docstring.
Subclasses set name and needs, may take options in __init__
(set them before calling super().__init__(data), since binding may
read them), and implement statistic. The optional _prepare hook
runs once after binding, for sufficient statistics.
Source code in src/ergodic/discovery/_ci.py
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | |
data
property
¶
The dataset this test is bound to, after dropping incomplete rows.
n
property
¶
Rows the oracle answers from (after dropping incomplete rows).
variables
property
¶
The variables this oracle answers about, sorted.
bind(data)
¶
A new instance of this test bound to data; never mutates self.
Source code in src/ergodic/discovery/_ci.py
96 97 98 99 100 | |
diagnostics()
¶
Run counters: total queries, largest conditioning set, cache hits.
Source code in src/ergodic/discovery/_ci.py
146 147 148 149 150 151 152 153 154 | |
independent(x, y, given=(), *, alpha)
¶
The decision the algorithms read: fail to reject at alpha.
Source code in src/ergodic/discovery/_ci.py
140 141 142 143 144 | |
statistic(*args)
¶
Return (statistic, p_value, dof); signature depends on needs.
numeric: statistic(x, y, z) with x (n, dx), y
(n, dy), z (n, dz) float blocks. codes:
statistic(x, y, z, cards) with x, y (n,) integer
codes, z a tuple of (n,) code columns, and cards the
matching (card_x, card_y, cards_z).
Source code in src/ergodic/discovery/_ci.py
158 159 160 161 162 163 164 165 166 167 | |
test(x, y, given=())
¶
The cached conditional-independence query, symmetric in x and y.
Source code in src/ergodic/discovery/_ci.py
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | |
DSeparationOracle
¶
Bases: CITest
The ground-truth oracle: answers queries by m-separation in a known graph.
Quacks like any bound test (p-value 1.0 when separated, 0.0 when
connected), so every constraint algorithm can be checked for exactness and
run in teaching mode. Needs no data; bind returns self.
Latent and selection nodes are not query-able variables: the oracle exposes the observed margin, which is what a constraint algorithm searching over data would see. Separation is still computed on the full graph, latents included, and every query conditions on the selection nodes, because data sampled under selection only ever shows the distribution given them.
Source code in src/ergodic/discovery/_ci.py
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | |
DirectLiNGAM
¶
DirectLiNGAM over continuous tabular data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prune
|
str
|
|
'bic'
|
fit(data, knowledge) returns a :class:DiscoveryResult whose graph
is a DAG: with independent non-Gaussian noises the whole orientation is
identified, so there is no equivalence class to report. The class
consumes neither a CI test nor a decomposable score (ingredient is
None); the data is its only ingredient.
Source code in src/ergodic/discovery/algorithms/_lingam.py
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 | |
fit(data, knowledge=None)
¶
Find the causal order, prune the parents, and return the DAG.
Source code in src/ergodic/discovery/algorithms/_lingam.py
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | |
DiscoveryResult
dataclass
¶
What a discovery run found, and how.
graph is the headline. test or score names the ingredient the
algorithm consumed (one of them is None). diagnostics carries the
run counters (tests by conditioning size, cache hits, score evaluations).
sepsets maps an unordered removed pair to the set that separated it,
when a constraint method produced one.
Source code in src/ergodic/discovery/_result.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | |
Discrete
¶
Bases: CITest
The G squared conditional-independence test on contingency tables.
Degrees of freedom are adjusted for sparsity, bnlearn-style: each
nonempty stratum of the conditioning configuration contributes
(card_x - 1) * (card_y - 1), and configurations with no observed rows
contribute nothing. The unadjusted product over the full configuration
space would overstate the dof badly on sparse tables and make the test
blind. When the dof comes out zero (a single-level endpoint, or no rows),
the query is vacuous and the result is (0.0, 1.0, 0.0).
Source code in src/ergodic/discovery/ci/_discrete.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
DiscreteBIC
¶
Bases: Score
The multinomial BIC, decomposable and score-equivalent.
Every column must be discrete (categorical, ordinal, or binary): the
score runs on integer codes, so binding to data with a continuous column
raises TypeError. Parameter counts agree across a Markov equivalence
class, so Markov-equivalent DAGs get equal totals, exactly as with the
Gaussian BIC.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
TabularData | None
|
Bind at construction, or later through |
None
|
penalty
|
float
|
A multiplier on the complexity term |
1.0
|
Source code in src/ergodic/discovery/scores/_discrete_bic.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
penalty
property
¶
The multiplier on the complexity term.
local_from_codes(y, y_card, zs, z_cards)
¶
The multinomial local from integer codes; see the module docstring.
Source code in src/ergodic/discovery/scores/_discrete_bic.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
FCI
¶
FCI over a conditional-independence test, returning a PAG.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
test
|
CITest | str
|
A :class: |
'auto'
|
alpha
|
float
|
Significance level for the independence decisions. |
0.05
|
max_cond_size
|
int | None
|
Cap on the conditioning-set size, applied to the
skeleton phase and to the possible-d-sep subsets alike; |
None
|
fit(data, knowledge) returns a :class:DiscoveryResult whose graph
carries the PAG face when the result is a valid complete PAG and the
mixed face otherwise (noisy tests or knowledge-seeded marks). With a
perfect oracle and no knowledge the output is exactly the canonical PAG:
pag_of_mag(mag_of_dag(truth)).
Source code in src/ergodic/discovery/algorithms/_fci.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
fit(data=None, knowledge=None)
¶
Run FCI and return the discovered graph with its provenance.
Source code in src/ergodic/discovery/algorithms/_fci.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | |
GCM
¶
Bases: CITest
The generalized covariance measure over a regression learner.
Regresses x and y on z with a fresh clone of the learner per endpoint,
forms the elementwise residual product, and reports
sqrt(n) * mean(R) / sd(R) against the standard normal, two-sided.
Residuals are in-sample, as in the paper's basic form. With an empty
conditioning set the endpoints are centered instead, so the statistic is
a plain covariance test.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
TabularData | None
|
The dataset to bind, or |
None
|
learner
|
Any
|
The regression slot, accepting exactly what an estimator's
learner slot accepts: any sklearn-style regressor or an alias
string like |
None
|
Source code in src/ergodic/discovery/ci/_gcm.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
GES
¶
Greedy equivalence search over a decomposable score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
score
|
Score | str
|
A :class: |
'auto'
|
max_degree
|
int | None
|
A cap on the number of edges at any node; an Insert
that touches a node already at the cap is never considered.
|
None
|
fit(data, knowledge) returns a :class:DiscoveryResult whose graph
carries the CPDAG face when the final PDAG is a valid CPDAG and the
mixed face otherwise (forced knowledge orientations can leave the
class; diagnostics["cpdag_valid"] says which happened).
Source code in src/ergodic/discovery/algorithms/_ges.py
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 | |
fit(data=None, knowledge=None)
¶
Run the forward and backward phases and return the class found.
Source code in src/ergodic/discovery/algorithms/_ges.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
GRaSP
¶
Greedy relaxations of the sparsest permutation over a decomposable score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
score
|
Score | str
|
A :class: |
'auto'
|
depth
|
int
|
How many levels of tied covered tucks the walk may chain before backtracking; the paper's experiments use 3. |
3
|
fit(data, knowledge) returns a :class:DiscoveryResult whose graph
carries the CPDAG face when the final PDAG is a valid CPDAG and the
mixed face otherwise (forced knowledge orientations can leave the
class; diagnostics["cpdag_valid"] says which happened).
Source code in src/ergodic/discovery/algorithms/_grasp.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | |
fit(data=None, knowledge=None)
¶
Walk tucks to convergence and return the class found.
Source code in src/ergodic/discovery/algorithms/_grasp.py
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | |
Glasso
¶
The graphical-lasso screen: an undirected skeleton from a sparse precision.
Like MMPC, this is a cheap screen to run before an expensive search:
DomainKnowledge().restrict_to(result.graph) turns the recovered
support into a constraint that PC, GES, or AStar then searches inside,
with far fewer tests or score calls. The honesty note: a precision zero
is a conditional independence under Gaussianity, so the graph is a
skeleton source, not causal structure; it says which adjacencies the
data supports, never which way they point.
The solve is FHT 2008 block coordinate descent on the correlation matrix (the graph, not the scale, is the deliverable), with the diagonal unpenalized (the scikit-learn convention; the paper's differs, see the module docstring) and the shrunk initialization that keeps every column subproblem positive definite.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lam
|
float | None
|
The penalty. |
None
|
gamma
|
float
|
The EBIC sparsity weight (Foygel and Drton 2010), |
0.5
|
n_lambdas
|
int
|
Path length when |
100
|
tol
|
float
|
Outer convergence, FHT's criterion: the mean absolute change of
the working covariance over a full cycle must drop under |
0.001
|
max_iter
|
int
|
Cap on outer cycles per solve; past it the fit raises
|
100
|
Knowledge enters through the penalty, and only its adjacency content
binds, because the screen is undirected. A pair forbidden in both
directions (an adjacency ban, including pairs outside a restrict_to
support) is a hard zero, excluded from the updates entirely. A required
pair (either direction) is unpenalized, so it survives every lambda the
data gives it any conditional association at. Directional knowledge
(tiers, one-way forbids, roots, sinks) does not constrain an undirected
adjacency and is left to the search that runs inside the support.
fit(data, knowledge) needs a :class:TabularData whose variables are
all continuous (a Gaussian model) and returns a :class:DiscoveryResult
whose graph holds one tail-tail edge per nonzero precision pair, on the
mixed face. The chosen penalty lives in the diagnostics, with the
EBIC value when the path ran.
Source code in src/ergodic/discovery/algorithms/_glasso.py
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 | |
fit(data, knowledge=None)
¶
Run the screen and return the undirected skeleton with its provenance.
Source code in src/ergodic/discovery/algorithms/_glasso.py
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 | |
GrangerResult
dataclass
¶
Granger tests for every ordered pair: a predictive table, not a graph.
pvalues maps (cause, effect) to the likelihood-ratio p-value;
significant holds the pairs with p <= alpha. n counts the
stacked window rows the regressions used.
Source code in src/ergodic/discovery/_granger.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
HillClimb
¶
Greedy DAG search: add, remove, and reverse moves with a tabu ring.
Steepest ascent from the empty (or knowledge-seeded) graph, returning
the best state seen. n_iterations in the diagnostics counts applied
moves, so it always equals n_adds + n_removes + n_reverses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
score
|
Score | str
|
A :class: |
'auto'
|
tabu
|
int
|
The length of the tabu ring (inverses of recent moves that may
not be replayed) and the bound on consecutive non-improving
escape steps; |
10
|
max_iter
|
int
|
A hard cap on the number of applied moves. |
10000
|
max_parents
|
int | None
|
An in-degree cap enforced on adds and reversals;
|
None
|
Source code in src/ergodic/discovery/algorithms/_hc.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | |
fit(data=None, knowledge=None)
¶
Climb to a high-scoring DAG and return it with its provenance.
Source code in src/ergodic/discovery/algorithms/_hc.py
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | |
Linear
¶
Bases: CITest
Partial correlation through the Fisher z transform.
Residualizes x and y on [1, z], computes the correlation r of the
residuals, and reports sqrt(n - dz - 3) * atanh(r) against the
standard normal, two-sided. The sign of r survives in the statistic;
the p-value does not depend on it.
Source code in src/ergodic/discovery/ci/_linear.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
MMPC
¶
The max-min parents-and-children screen over a conditional-independence test.
MMPC is a cheap skeleton screen to run before an expensive search: it
returns the undirected adjacencies the data supports, and
DomainKnowledge().restrict_to(result.graph) turns them into a support
that PC, GES, or AStar then searches inside, with far fewer tests or
score calls. Adjacencies are kept under the AND rule (each endpoint must
sit in the other's parents-children set), the stricter of the two
symmetry choices, so the screen errs toward sparsity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
test
|
CITest | str
|
A :class: |
'auto'
|
alpha
|
float
|
Significance level for the independence decisions. |
0.05
|
max_cond_size
|
int | None
|
Cap on the conditioning-set size, |
3
|
Knowledge enters in three modes. Prune: a pair forbidden in both
directions is never a candidate. Protect: required and confounded pairs
enter every CPC untested, survive the backward sweep, and pass the AND
rule unconditionally. Validate: the finished skeleton is checked at the
adjacency level only, because a skeleton has no directions, so
is_consistent (which demands required edges be directed) cannot
apply; orientation constraints (tiers, single-direction forbids, roots,
sinks) do not constrain an undirected adjacency and are left to the
search that runs inside the support.
fit(data, knowledge) returns a :class:DiscoveryResult whose graph
holds one tail-tail edge per surviving pair, with the mixed face.
Source code in src/ergodic/discovery/algorithms/_mmpc.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | |
fit(data=None, knowledge=None)
¶
Run MMPC and return the undirected skeleton with its provenance.
Source code in src/ergodic/discovery/algorithms/_mmpc.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | |
Mixed
¶
Bases: CITest
Partial canonical correlations with Bartlett's chi-square.
Residualizes the x and y blocks on [1, z], takes the reduced QR of
each residual block, and reads the canonical correlations rho_i off
the singular values of Qx.T @ Qy (clipped below 1). Bartlett's
statistic -(n - dz - (dx + dy + 3) / 2) * sum(log(1 - rho_i^2)) is
referred to a chi-square with dx * dy degrees of freedom.
With scalar endpoints (dx == dy == 1) the single canonical correlation
is the absolute partial correlation, so Mixed agrees with Linear
asymptotically. A zero-width block (a single-level categorical) carries no
information; that query is vacuous and returns (0.0, 1.0, 0.0).
Source code in src/ergodic/discovery/ci/_mixed.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
MixedBIC
¶
Bases: BIC
The degenerate-Gaussian BIC, decomposable and score-equivalent.
Any column kind is a valid node: a continuous node is its single column
and a categorical node is its one-hot block (first level dropped), scored
jointly. local = -(n/2) log det Sigma_{Y|X} - penalty * p/2 * log n
with p = dy dx + dy (dy + 1) / 2 for a dy-column node and dx
total parent columns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
TabularData | None
|
Bind at construction, or later through |
None
|
penalty
|
float
|
A multiplier on the complexity term, inherited from |
1.0
|
Source code in src/ergodic/discovery/scores/_mixed_bic.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
local_from_arrays(y, X)
¶
The same local by direct least squares on (n, dy) y and (n, k) X.
Agrees with the covariance path to numerical precision; the tests hold
the two to 1e-8.
Source code in src/ergodic/discovery/scores/_mixed_bic.py
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
PC
¶
PC-stable over a conditional-independence test.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
test
|
CITest | str
|
A :class: |
'auto'
|
alpha
|
float
|
Significance level for the independence decisions. |
0.05
|
max_cond_size
|
int | None
|
Cap on the conditioning-set size, |
None
|
colliders
|
str
|
The v-structure rule: |
'standard'
|
fit(data, knowledge) returns a :class:DiscoveryResult whose graph
carries the CPDAG face when the oriented PDAG is a valid CPDAG and the
mixed face otherwise.
Source code in src/ergodic/discovery/algorithms/_pc.py
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | |
fit(data=None, knowledge=None)
¶
Run PC-stable and return the discovered graph with its provenance.
Source code in src/ergodic/discovery/algorithms/_pc.py
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | |
PCMCI
¶
PCMCI+ over a conditional-independence test, for time-series and panel data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
test
|
CITest | str
|
A :class: |
'auto'
|
alpha
|
float
|
Significance level for every independence decision. |
0.05
|
max_lag
|
int
|
Largest lag a reported link can have, at least one. |
1
|
max_cond_size
|
int | None
|
Cap on the screen round and on the contemporaneous
subset size, |
None
|
fit(data) takes :class:~ergodic.data.TimeSeriesData or
:class:~ergodic.data.PanelData (panel windows stack per entity) and
returns a :class:TimeSeriesDiscoveryResult whose graph lives on the
window nodes v[t-k].
fit(data, knowledge) reads a
:class:~ergodic.knowledge.DomainKnowledge over the process names, not
the window nodes, so a constraint on (x, y) applies at every lag,
the contemporaneous slice included:
- A directional forbid
x -> ymeans the link(x, y, lag)is never screened or MCI-tested at any lag and the lag-zero orientationx -> yis banned; the contemporaneous edge itself may survive wheny -> xstays permitted (only the direction dies). A pair forbidden in both directions is an adjacency ban: the contemporaneous pair is removed without testing (and records no sepset) and both lagged directions are pruned. An explicitforbid(x, x)removes the self-memory linkx[t-1] -> x[t]. - Tiers and
order: a later variable never causes an earlier one at any lag, pruned exactly as a forbid; additionally a surviving contemporaneous edge between ordered processes is seeded earlier->later before the collider phase, and a data-driven arrowhead that disagrees follows the orientation-conflict convention already in this file (the edge stays undirected and is counted indiagnostics["orientation_conflicts"]). - Roots take no incoming cross-variable link at any lag and sinks no
outgoing one; self-memory
x[t-1] -> x[t]survives both, since a process can carry its own past without being caused by anything else. A skeleton restriction (restrict_to) bans pairs outside its support the same way, self-memory exempt. required (x, y)names no lag, so it cannot be forced; it is validated after the run instead, andfitraisesValueErrorwhen no compatible link (a laggedx -> y, an oriented lag-zerox -> y, or a still-undirected lag-zero edge) survived.confoundhas no analogue in a window graph (it carries no bidirected edges) and raisesNotImplementedError.
diagnostics["n_knowledge_pruned"] counts what the knowledge took
off the table: lagged links never screened, contemporaneous pairs
removed outright, and lag-zero direction bans, one count each.
Source code in src/ergodic/discovery/algorithms/_pcmci.py
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 | |
fit(data=None, knowledge=None)
¶
Run PCMCI+ and return the window graph with its provenance.
knowledge constrains processes, so every constraint applies at
every lag; the class docstring lists the mappings. confound
raises here (a window graph has no bidirected edges), and a
required edge that survived at no lag raises after the run.
Source code in src/ergodic/discovery/algorithms/_pcmci.py
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | |
RFCI
¶
RFCI over a conditional-independence test: FCI's marks at PC's price.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
test
|
CITest | str
|
A :class: |
'auto'
|
alpha
|
float
|
Significance level for the independence decisions. |
0.05
|
max_cond_size
|
int | None
|
Cap on the conditioning-set size in the skeleton
phase; |
None
|
fit(data, knowledge) returns a :class:DiscoveryResult whose graph
carries the PAG face when the output happens to be a valid complete PAG
and the mixed face with diagnostics["pag_valid"] = 0.0 otherwise.
Unlike FCI, the mixed face is an expected honest outcome here even under
a perfect oracle: the RFCI skeleton can keep edges only the
possible-d-sep phase would remove, and the result then stands for more
than one Markov equivalence class. Domain knowledge enters exactly as in
FCI: prune (a pair forbidden in both directions is never tested), protect
(required and confounded pairs are never tested or removed), orient
through ancestry (the same seeding helpers), validate at the end.
Source code in src/ergodic/discovery/algorithms/_rfci.py
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 | |
fit(data=None, knowledge=None)
¶
Run RFCI and return the discovered graph with its provenance.
Source code in src/ergodic/discovery/algorithms/_rfci.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
Score
¶
Base for decomposable scores; see the module docstring.
Subclasses set name, needs, and the property flags, may take
options in __init__ (set them before calling super().__init__(data)),
and implement the method matching needs: local_from_arrays for
"numeric", local_from_codes for "codes". _prepare runs once
after binding for sufficient statistics; a native score may also override
_local_value to skip array assembly entirely (the covariance path in
BIC).
Source code in src/ergodic/discovery/_score.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | |
data
property
¶
The dataset this score is bound to, after dropping incomplete rows.
variables
property
¶
The variables this oracle answers about, sorted.
bind(data)
¶
A new instance of this score bound to data; never mutates self.
Source code in src/ergodic/discovery/_score.py
90 91 92 93 94 | |
diagnostics()
¶
Run counters: evaluations and cache hits.
Source code in src/ergodic/discovery/_score.py
165 166 167 | |
enumerate_locals(node, candidates, max_parents=None)
¶
Every candidate parent set up to max_parents, scored.
The access pattern of the exact solvers (A*, dynamic programming, ILP): all the local scores up front under an in-degree cap.
Source code in src/ergodic/discovery/_score.py
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | |
graph_score(parents_of)
¶
The decomposable total: the sum of locals over a parent map.
Source code in src/ergodic/discovery/_score.py
161 162 163 | |
local(node, parents=())
¶
The cached local score of node given parents.
Source code in src/ergodic/discovery/_score.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
local_diff(node, parents, add)
¶
The gain from adding add to the parents of node.
The default is two locals; an implementation may override it with a cheaper incremental form.
Source code in src/ergodic/discovery/_score.py
135 136 137 138 139 140 141 142 | |
local_from_arrays(y, X)
¶
The local score from arrays: y is (n, dy), X is (n, k).
Source code in src/ergodic/discovery/_score.py
171 172 173 | |
local_from_codes(y, y_card, zs, z_cards)
¶
The local score from codes: y is (n,) integer codes, zs the parents'.
Source code in src/ergodic/discovery/_score.py
175 176 177 178 179 180 181 182 183 | |
TimeSeriesDiscoveryResult
dataclass
¶
Bases: DiscoveryResult
A :class:DiscoveryResult over window nodes, with the per-link p-values.
links maps (source, target, lag) to the p-value of the test that
kept the link. A lag of one or more is the lagged edge
source[t-lag] -> target[t]. A lag of zero is a contemporaneous edge:
an oriented edge is keyed (source, target), an undirected edge is
keyed once with the pair in sorted order.
Source code in src/ergodic/discovery/_tsresult.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
summary_edges()
¶
Variable-level pairs implied by the links.
(x, y) is present when any lagged link x -> y exists or a
contemporaneous edge x -> y is oriented in the window graph. An
undirected contemporaneous pair contributes both orders, and a lagged
self-link (autocorrelation) contributes (x, x).
Source code in src/ergodic/discovery/_tsresult.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
bootstrap(data, *, method='pc', n_bootstrap=100, seed=0, knowledge=None, block_length=None, **kwargs)
¶
Rerun a discovery method on bootstrap resamples and count what survives.
Answers the question a single run cannot: how stable is this edge under
resampling? Each replicate draws a resample, runs
discover(sample, method=method, knowledge=knowledge, **kwargs), and
the result aggregates adjacency and glyph frequencies over the runs that
finished. Extra keyword arguments flow to the algorithm unchanged
(alpha, max_cond_size, score=..., max_lag=...), so any
registered method, iid or temporal, sits behind the same call.
An iid method on a :class:~ergodic.data.TabularData draws
data.n_rows rows with replacement, the classic bootstrap;
block_length does not apply there and raises. A temporal method
("pcmci") never resamples rows, which would shred the serial
dependence it reads. On a :class:~ergodic.data.TimeSeriesData it runs a
moving-block bootstrap: ceil(T / L) blocks of L consecutive rows
(block_length, default max(ceil(sqrt(T)), 2 * (max_lag + 1))) are
drawn with replacement and enter the replicate as separate entities of a
panel, "block_0", "block_1", ... with fresh time stamps.
Blocks-as-entities is the point: gluing blocks back into one series would
build lagged windows that straddle two unrelated fragments and let fake
adjacencies in, while per-entity pooling never reads across a block seam.
On a :class:~ergodic.data.PanelData it runs the cluster bootstrap
instead, drawing n_entities entities whole with replacement
(relabeled in draw order so repeated draws stay distinct);
block_length does not apply to a panel and raises. One honesty note:
block resampling treats the series as stationary at the block scale, so a
trend or a regime change inside the sampling window leaks into the
frequencies. An iid method on temporal data still refuses, inside the
replicate, exactly as :func:discover does.
The frequencies measure resampling variability, not Bayesian uncertainty;
they are not posterior probabilities over graphs (graph posteriors belong
to a later phase). The run is deterministic: the same data, seed, and
settings give the same result. A temporal method keys the frequencies on
window-graph glyphs like "X[t-1] --> Y[t]".
A replicate that raises ValueError or RuntimeError (degenerate
resamples happen: a constant column, a knowledge conflict on noisy data)
counts in n_failed and is skipped; frequencies are over the successful
runs only. When every replicate fails, the wrapper raises RuntimeError
chaining the last error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
TabularData | TimeSeriesData | PanelData
|
The dataset to resample: tabular for the iid methods, a time series or panel for the temporal ones. |
required |
method
|
str
|
A registered discovery method name, as in :func: |
'pc'
|
n_bootstrap
|
int
|
How many resamples to draw, at least 1. |
100
|
seed
|
int
|
Seed for the resampling generator. |
0
|
knowledge
|
DomainKnowledge | None
|
Hard constraints, handed to every replicate. |
None
|
block_length
|
int | None
|
Moving-block length for a time series under a temporal
method; |
None
|
**kwargs
|
Any
|
Passed through to |
{}
|
Source code in src/ergodic/discovery/_bootstrap.py
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | |
discover(data, method='pc', *, test=None, score=None, knowledge=None, alpha=0.05, **kwargs)
¶
Learn causal structure from data with the named method.
test feeds a constraint method (a :class:CITest instance, bound or
not, or an alias like "linear"); score feeds a score-based method
(a :class:Score instance or an alias like "bic", "bdeu"); both
default to "auto", picked from the column schema. Each algorithm
declares which ingredient it consumes. Extra keyword arguments pass to
the algorithm (max_cond_size, max_parents, node_limit,
max_degree, tabu, max_lag).
The iid methods read a :class:TabularData and reject temporal shapes
(rows are not exchangeable); method="pcmci" reads a
:class:TimeSeriesData or :class:PanelData and returns a window graph
over lagged nodes like "X[t-1]".
A few methods consume neither ingredient because the machinery is the
algorithm: "lingam" (regression and residual independence) and
"glasso" (a sparse precision estimate). They reject test= and
score=, and alpha does not apply (their own knobs, like lam
or prune, pass through the keyword arguments).
Source code in src/ergodic/discovery/_discover.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | |
granger(data, *, max_lag=1, alpha=0.05)
¶
Granger-test every ordered pair of variables in data.
For each effect Y, the full regression puts Y[t] on an intercept
plus every variable's lags 1..max_lag; the restricted one drops the
candidate cause's lags. All variables must be continuous. See the module
docstring for what this does and does not mean.
Source code in src/ergodic/discovery/_granger.py
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
lagged_node(name, lag)
¶
The window-column name for name at lag steps back.
Lag zero reads "X[t]", lag k reads "X[t-k]". The name must
carry no whitespace, since the graph layer parses edges on whitespace.
Source code in src/ergodic/discovery/_lagged.py
28 29 30 31 32 33 34 35 36 37 38 39 40 | |
lagged_tabular(data, max_lag)
¶
Stack the windows of data into a tabular frame of lagged columns.
Row t holds every variable at lags 0..max_lag; the column for
variable v at lag k is named lagged_node(v, k) and holds v
at time t - k. Column types carry over: continuous stays continuous,
categorical keeps its categories, ordinal keeps its level order.
A time series needs at least max_lag + 8 rows. Panel windows are
built within each entity (sorted entity order) and stacked; entities with
fewer than max_lag + 2 rows are skipped with a warning, and it is an
error when no rows survive. Unequal time steps warn too: lags count rows,
not calendar time.
Source code in src/ergodic/discovery/_lagged.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
parse_lagged(node)
¶
Invert :func:lagged_node: "X[t-2]" becomes ("X", 2).
Source code in src/ergodic/discovery/_lagged.py
43 44 45 46 47 48 49 50 51 52 53 54 | |
Causal inference¶
Effect estimation: the estimators, the learners that fill their slots, refutation, and the quasi-experimental designs.
ergodic.inference
¶
Causal inference: estimate causal effects from data and a causal model.
An estimator factors into independent pieces: the estimand family (from
:mod:ergodic.identification), the strategy that turns it into a number (a
:class:CausalEstimator), and the learner in each nuisance slot. estimate_effect
runs identification then estimation and returns an :class:EffectEstimate.
>>> from ergodic import dag, tabular
>>> from ergodic.inference import estimate_effect
>>> est = estimate_effect(graph, data, "T", "Y") # doctest: +SKIP
>>> est.value, est.interval() # doctest: +SKIP
The learner is structurally a sklearn estimator, so estimate_effect(..., learner="knn")
or a slot mapping like learner={"outcome": GradientBoostingRegressor()} drops
any sklearn or xgboost model into the slots with no adapter. The result is
distribution-shaped: a point estimate with an analytic standard error, or
bootstrap draws when there is no closed form.
The quasi-experimental designs read panels and time series instead of a graph:
:func:did and :func:event_study over a :class:~ergodic.data.PanelData
(parallel trends), :func:synthetic_control (a convex donor combination), and
:func:interrupted_time_series over a :class:~ergodic.data.TimeSeriesData
(a projected pre-trend). Each states its identification in a :class:Design
record and returns the same :class:EffectEstimate surface.
AIPW
¶
Bases: _AdjustmentBase
Combine an outcome model and a propensity model.
Consistent if either model is right. The ATE standard error comes from the efficient influence function (Neyman-orthogonal, so first-order insensitive to nuisance error); the ATT standard error falls back to bootstrap.
Source code in src/ergodic/inference/estimators/_doubly_robust.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | |
BayesianLearner
¶
Bases: Protocol
A learner that also carries a posterior over its mean function.
Structurally a :class:Learner (fit then predict, where predict is
the posterior predictive mean), plus predict_draws returning (S, n)
draws of the mean function. A classifier variant exposes predict_proba and
predict_proba_draws (draws of the class-1 probability) instead. Detection is
duck-typed in :func:_is_bayesian, the same way a classifier is spotted by
predict_proba, so any object that quacks this way counts.
Because predict stays the posterior mean, a Bayesian learner is also a valid
point learner in every estimator; the posterior path is an addition, not a fork.
Source code in src/ergodic/inference/_learner.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
CATEResult
dataclass
¶
Bases: PerUnitEffect
Conditional average treatment effects, with an explainability hook.
A conditional average over the unobserved noise (the inference-pillar object).
Carries the :class:~ergodic.identification.Identification it was fit under and
the cached doubly-robust scores + covariate design, so :meth:explain projects
honest summaries (BLP, GATES, CLAN, the calibration test, RATE) off the same
orthogonal signal.
Source code in src/ergodic/inference/_perunit.py
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | |
explain()
¶
Tier-B characterization of the heterogeneity, on the doubly-robust scores.
Builds a :class:~ergodic.inference.explain.Heterogeneity record whose
methods (blp, gates, clan, calibration_test, rate) all
consume the cached cross-fit scores, the covariate design, and the fitted
per-unit tau_hat. Needs the scores; raises when they were not cached
(an estimator that does not build them, or a result without them).
Source code in src/ergodic/inference/_perunit.py
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | |
CausalEstimator
¶
Bases: ABC
Base for the strategies. Stateful: fit then effect.
Source code in src/ergodic/inference/_estimator.py
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 | |
effect(*, target=None)
¶
Report the effect, with an analytic standard error or a bootstrap one.
Source code in src/ergodic/inference/_estimator.py
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 | |
fit(identification, data)
¶
Read the estimand and the data, resolve learners, store the fit state.
Source code in src/ergodic/inference/_estimator.py
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | |
CausalForest
¶
Bases: _MetaLearnerBase
An honest causal forest (GRF) for the conditional average treatment effect.
The forest is the CATE learner. The outcome and propensity nuisance slots
take any learner (they residualize Y and W on the covariates before the
forest grows); the forest machinery, honest splitting and the kernel readout, is
native. The ATE is the average per-unit effect, and cate returns a
:class:~ergodic.inference.CATEResult with per-unit infinitesimal-jackknife
standard errors and intervals.
Hyperparameters: n_trees (forest size), min_leaf (the minimum estimate-half
rows per leaf), max_depth, honest (split/estimate sample-splitting, on by
default), subsample (the per-tree subsample fraction), and random_state.
Binary treatment in this phase; a continuous treatment via the same local moment
is deferred and raises.
Source code in src/ergodic/inference/estimators/_forest.py
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 | |
cate(data=None, *, uncertainty=True)
¶
Per-unit CATE with infinitesimal-jackknife standard errors and intervals.
Overrides the base meta-learner cate (which would bootstrap-refit the
whole forest). The point is the forest-kernel local effect; the per-unit
variance is the infinitesimal jackknife over the trees (the
bootstrap-of-little-bags grouping), valid because the forest is honest and
subsampled. The result carries the cached doubly-robust scores for
:meth:~ergodic.inference.CATEResult.explain.
uncertainty=False returns the point effects with kind="point" and no
standard error, skipping the infinitesimal-jackknife pass over the trees.
Source code in src/ergodic/inference/estimators/_forest.py
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 | |
variable_importance()
¶
Depth-weighted, normalized split frequencies over the covariates.
A driver signal: which covariates the forest splits on, weighted toward the
shallow splits that move the effect most. Available after fit.
Source code in src/ergodic/inference/estimators/_forest.py
626 627 628 629 630 631 632 633 | |
DML
¶
Bases: _AdjustmentBase
Double machine learning, the partially linear model (partialling-out).
Residualize the outcome and the treatment against the covariates with cross-fitting, then regress one residual on the other. The treatment may be binary or continuous; a continuous treatment reports the average partial effect. The ATE standard error comes from the orthogonal moment.
Source code in src/ergodic/inference/estimators/_dml.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | |
cate(data=None, *, uncertainty=True)
¶
The constant per-unit effect for the partially-linear model: theta.
The partially-linear DML assumes a homogeneous effect, so every unit's CATE
is the single slope theta, with the same analytic standard error from the
orthogonal moment. Returned as a :class:~ergodic.inference.CATEResult for a
uniform surface; a binary treatment additionally caches the doubly-robust
scores so explain can test for the heterogeneity the model assumes away.
uncertainty=False returns the point theta with kind="point" and no
standard error.
Source code in src/ergodic/inference/estimators/_dml.py
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | |
DRLearner
¶
Bases: _MetaLearnerBase
Doubly-robust learning of the ATE and the CATE.
Cross-fit the AIPW score into a per-unit pseudo-outcome, average it for the
ATE (with the influence-function standard error), and regress it on the
covariates for the CATE through cate. The CATE result carries the cached
doubly-robust scores, so its explain runs the Tier-B characterization.
Source code in src/ergodic/inference/estimators/_dml.py
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | |
DRScores
dataclass
¶
The cross-fit doubly-robust object: the AIPW signal plus its nuisances.
scores is the per-unit Gamma_i (its mean is the AIPW ATE). mu0,
mu1, and propensity are the cross-fit out-of-fold nuisances, kept so the
calibration and projection tools reuse them without refitting. treatment,
outcome, and covariates carry the design back to the result.
Source code in src/ergodic/inference/_scores.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | |
Design
dataclass
¶
How a quasi-experimental estimator claims identification.
strategy is one of "did", "synthetic_control", or
"interrupted_time_series". assumptions are the untestable claims the
design rests on, stated in words (parallel trends, convex donor span, no
co-timed shock); they are what a reader should challenge, so they ride with
every result.
Source code in src/ergodic/inference/_design.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | |
DiDEffect
dataclass
¶
Bases: EffectEstimate
A difference-in-differences result: the estimate plus the design it rests on.
The :class:EffectEstimate surface (value, std_error, interval,
draws) reads the headline ATT. flavor records which estimator ran,
comparison which pool stood in for the counterfactual, by_group_time
the per-cell detail of the group-time flavor, and by_event_time the
event-study aggregation when :func:event_study built the result.
Source code in src/ergodic/inference/estimators/_did.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
EffectEstimate
dataclass
¶
A causal effect with its uncertainty.
Built by a :class:CausalEstimator. Either _point/_std_error are set
(an analytic result) or _draws is set (bootstrap or posterior), and the
accessors read whichever is present.
Source code in src/ergodic/inference/_result.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
draws
property
¶
The bootstrap or posterior draws, or None for a pure analytic result.
std_error
property
¶
The analytic standard error, or the standard deviation of the draws.
value
property
¶
The point estimate, or the mean of the draws.
interval(level=0.95)
¶
A confidence or credible interval at level.
From the draw quantiles when draws are present, else the normal interval around the point estimate.
Source code in src/ergodic/inference/_result.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | |
EffectTarget
¶
Bases: Enum
Which effect a :class:CausalEstimator reports.
E1 returns ATE and ATT for the adjustment and front-door families and
LATE for instruments. ATC, CATE, and ITE are reserved for the
heterogeneous-effect phase.
Source code in src/ergodic/inference/_target.py
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
EventTimeEffect
dataclass
¶
The effect at one event time, aggregated over cohorts with cohort-size weights.
std_error and interval come from the unit bootstrap: the standard
deviation of the draws and their 95% percentile interval. n_treated
counts the treated units contributing a cell at this event time.
Source code in src/ergodic/inference/estimators/_did.py
67 68 69 70 71 72 73 74 75 76 77 78 79 80 | |
GroupTimeEffect
dataclass
¶
One ATT(g, t) cell: the cohort adopting at group, read at period.
event_time is the position of period minus the position of group
in the sorted period sequence, so -1 is the base period and never appears.
n_treated and n_comparison count the units observed at both the base
and the target period on each side of the comparison.
Source code in src/ergodic/inference/estimators/_did.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
Heterogeneity
dataclass
¶
Tier-B heterogeneity characterization on shared doubly-robust scores.
Carries the cross-fit :class:~ergodic.inference._scores.DRScores and the fitted
per-unit tau_hat. The honesty register: the rankings and proxies are fixed
(computed cross-fit), so the orthogonal summaries below inherit valid inference
while the CATE point predictions themselves never carry a confidence interval.
Source code in src/ergodic/inference/explain/_record.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
blp(*, level=0.9)
¶
Best Linear Projection of the CATE on the covariates (HC3 inference).
Source code in src/ergodic/inference/explain/_record.py
55 56 57 58 59 60 61 62 | |
calibration_test()
¶
The omnibus calibration / heterogeneity test (one-sided HTE p-value).
Source code in src/ergodic/inference/explain/_record.py
78 79 80 | |
clan(*, n_groups=5, level=0.9)
¶
Covariate-mean differences between the most- and least-affected groups.
Source code in src/ergodic/inference/explain/_record.py
68 69 70 71 72 73 74 75 76 | |
gates(*, n_groups=5, level=0.9)
¶
Sorted Group ATEs across n_groups predicted-effect quantiles.
Source code in src/ergodic/inference/explain/_record.py
64 65 66 | |
policy_tree(covariates=None, *, depth=2)
¶
The depth-limited, DR-welfare-optimal policy tree off these scores.
A convenience that forwards the shared doubly-robust scores to
:func:~ergodic.inference.policy_tree. covariates selects the split
columns by name (None uses all of them). The "act" companion to
:meth:rate: where rate scores a ranking, this returns an interpretable
treat-or-not rule and its estimated policy value.
Source code in src/ergodic/inference/explain/_record.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
rate(metric='autoc', *, n_boot=200, random_state=None)
¶
The Rank-Weighted ATE of the tau_hat ranking: "autoc" or "qini".
Source code in src/ergodic/inference/explain/_record.py
82 83 84 85 86 87 88 89 90 91 92 | |
IPW
¶
Bases: _AdjustmentBase
Weight by the inverse propensity of treatment.
scheme="hajek" (the default) self-normalizes the weights; scheme="ht"
is the raw Horvitz-Thompson estimator. Propensities are clipped to
[clip, 1 - clip] to bound the weights.
Source code in src/ergodic/inference/estimators/_weighting.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
ITEResult
dataclass
¶
Bases: PerUnitEffect
Individual treatment effects from a structural model (the twin network).
The individual's own counterfactual contrast (the SCM-pillar object), not a
conditional average. Carries the factual outcome column, the contrast framing
(levels= or shift=), and a structural provenance note.
:meth:explain is deferred: the Tier-B machinery is defined over the CATE's
doubly-robust scores, and the inference-valid descriptive regression of abducted
ITEs is a later phase, so it raises with a pointer rather than faking inference.
Source code in src/ergodic/inference/_perunit.py
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 | |
factual
property
¶
The per-unit factual outcome the contrast was taken against, when carried.
explain()
¶
Deferred in phase 1: the Tier-B tools need the CATE's doubly-robust scores.
Source code in src/ergodic/inference/_perunit.py
306 307 308 309 310 311 312 313 | |
ITSEffect
dataclass
¶
Bases: EffectEstimate
A segmented-regression fit around one break.
level_change is the jump at the break and slope_change the change
in trend per period, each with a Newey-West standard error. value is
the ATT: the average post-period gap between fitted and
counterfactual, where counterfactual projects the pre-period trend
over every period with the break terms zeroed and the seasonal terms kept.
Source code in src/ergodic/inference/estimators/_its.py
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
KNNClassifier
¶
Bases: _KNNBase
K-nearest-neighbors classification for a binary 0/1 label.
Source code in src/ergodic/inference/learners/_neighbors.py
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
KNNRegressor
¶
Bases: _KNNBase
K-nearest-neighbors regression: the prediction is the neighbor mean.
Source code in src/ergodic/inference/learners/_neighbors.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 | |
Learner
¶
Bases: Protocol
The sklearn estimator surface the estimators rely on.
Source code in src/ergodic/inference/_learner.py
27 28 29 30 31 32 33 34 35 36 37 38 | |
LogisticRegression
¶
Binary logistic regression by Newton-Raphson (IRLS), ridge-stabilized.
Source code in src/ergodic/inference/learners/_linear.py
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | |
MediationEstimate
dataclass
¶
A mediation decomposition with its uncertainty.
nde, nie, and total are the natural direct effect, the natural
indirect effect, and the total effect (nde + nie), each a distribution-
shaped :class:~ergodic.inference.EffectEstimate with a bootstrap standard
error and interval. cde maps each mediator level to the controlled direct
effect at that level (also an :class:EffectEstimate). design names the
(untestable) assumptions the decomposition rests on, the same role
:class:~ergodic.inference.Design plays for the quasi-experimental estimators.
proportion_mediated is nie / total, the share of the total effect that
flows through the mediator. It is unstable when the total effect is near zero
(a small denominator), so it is offered with that caveat and a flagged
diagnostic.
Source code in src/ergodic/inference/estimators/_mediation.py
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
proportion_mediated
property
¶
NIE / TE: the share of the total effect carried by the mediator.
Unstable when the total effect is near zero; see
:attr:proportion_mediated_flagged.
proportion_mediated_flagged
property
¶
True when the total effect is too small to trust the proportion.
The proportion mediated has the total effect in its denominator, so a
total effect close to zero makes it numerically unstable: a tiny shift in
either piece swings the ratio. Flagged when |TE| is below one bootstrap
standard error (or essentially zero).
NullEffect
¶
Bases: CausalEstimator
Report the exact zero effect for a null estimand.
Identification returns strategy="null" when the graph has no directed
path from the treatment to the outcome, so the intervention cannot move the
outcome and the effect is zero by construction. No nuisance slots, no
uncertainty: the point is 0.0 with a zero standard error.
Source code in src/ergodic/inference/estimators/_null.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | |
OLS
¶
Ordinary least squares, with optional ridge penalty and sample weights.
Source code in src/ergodic/inference/learners/_linear.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
OutcomeRegression
¶
Bases: _MetaLearnerBase
Fit a model for the outcome and average the predicted contrast.
With per_arm=False (the default) one model carries the treatment as a
feature (the S-learner / g-formula). With per_arm=True a separate model is
fit on each arm (the T-learner). A Bayesian outcome learner flows its posterior
into the per-unit CATE result and into cate_draws/cate_interval.
Source code in src/ergodic/inference/estimators/_regression.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
cate_draws(data=None)
¶
Per-unit CATE posterior (mu1 - mu0), shape (S, n).
Needs a Bayesian outcome learner; in-sample when data is None.
Source code in src/ergodic/inference/estimators/_regression.py
117 118 119 120 121 122 123 124 | |
cate_interval(data=None, *, level=0.95)
¶
Per-unit credible interval for the CATE: the column quantiles of the draws.
Source code in src/ergodic/inference/estimators/_regression.py
126 127 128 129 130 131 132 133 | |
PLIV
¶
Bases: _IVBase
Orthogonal instrumental variables (the partially linear IV model).
Residualize the outcome, treatment, and instrument against the covariates with cross-fitting, then the IV moment on the residuals. The flexible-learner instrument estimator that two-stage least squares is not. Reports LATE with an analytic standard error.
Source code in src/ergodic/inference/estimators/_iv.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
PerUnitEffect
dataclass
¶
Per-unit treatment effects with their uncertainty.
_values is the per-unit point vector (n,), ordered to the input rows.
Either _std_error (per-unit analytic SEs) or _draws (S, n) (bootstrap
or posterior) carries the uncertainty, and the accessors read whichever is
present. index are explicit row ids; estimand is set by the subclass.
Source code in src/ergodic/inference/_perunit.py
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | |
index
property
¶
The row ids for the units, positional arange(n) when none were given.
shape
property
¶
The shape of the per-unit point vector, (n,).
std_error
property
¶
Per-unit standard errors, or the per-unit draw standard deviations.
values
property
¶
The per-unit point estimates, (n,), ordered to the input rows.
When only draws are present, the posterior or bootstrap mean per unit.
__array__(dtype=None)
¶
The per-unit point vector, so np.asarray(result) and arithmetic work.
Source code in src/ergodic/inference/_perunit.py
80 81 82 83 | |
__getitem__(item)
¶
Index into the per-unit point vector (so result[i] is the unit's effect).
Source code in src/ergodic/inference/_perunit.py
90 91 92 | |
ate()
¶
The population summary: the average per-unit effect, as an EffectEstimate.
The bridge back to the scalar family. With draws, the per-unit draws average to a draw vector of the mean effect (so the population interval reflects the joint uncertainty). With per-unit analytic SEs, the point is the mean and the standard error combines them assuming independence. With neither, a point.
Source code in src/ergodic/inference/_perunit.py
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | |
draws()
¶
The (S, n) bootstrap or posterior draws, or None for a point result.
Source code in src/ergodic/inference/_perunit.py
156 157 158 159 160 | |
interval(level=0.9)
¶
Per-unit confidence or credible intervals, (lo[n], hi[n]) at level.
Draw quantiles when draws are present, else the normal interval from the per-unit point and standard error. Raises when there is no uncertainty.
Source code in src/ergodic/inference/_perunit.py
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
to_frame()
¶
A per-unit table (index, effect, and lo/hi/se when available). Pandas only here.
Source code in src/ergodic/inference/_perunit.py
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | |
PolicyTree
dataclass
¶
A shallow doubly-robust optimal-policy tree with its value and regret.
root is the learned tree over the named covariates; :meth:predict and
:meth:assign evaluate it. value/std_error are the estimated policy
value W(pi_hat) and its standard error from the per-unit influence terms;
treat_all_value and treat_none_value are the two trivial baselines, and
:meth:regret is the value gap against either. depth is the requested depth
and exhaustive records whether the globally optimal search was used.
Source code in src/ergodic/inference/explain/_policy.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
assign()
¶
The fitted policy's action for each unit it was learned on.
Source code in src/ergodic/inference/explain/_policy.py
123 124 125 | |
describe()
¶
A readable if-else rendering of the tree (the interpretable rule).
Source code in src/ergodic/inference/explain/_policy.py
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
interval(level=0.9)
¶
A normal confidence interval for the policy value at level.
Source code in src/ergodic/inference/explain/_policy.py
127 128 129 130 131 132 133 134 | |
leaves()
¶
The leaf nodes, left to right (each carries its action, count, and value).
Source code in src/ergodic/inference/explain/_policy.py
159 160 161 162 163 164 165 166 167 168 169 170 171 172 | |
predict(covariates)
¶
The per-row action (0/1) for a covariate matrix aligned to covariates.
Source code in src/ergodic/inference/explain/_policy.py
112 113 114 115 116 117 118 119 120 121 | |
regret(baseline='treat_all')
¶
The learned policy's welfare GAIN over a trivial baseline (the baseline's regret).
baseline is "treat_all", "treat_none", or "best_trivial" (the
better of the two). Returns value - baseline_value: how much welfare the
learned rule adds over treating everyone / no one. The trivial constant
policies are depth-0 leaves inside the tree's class, so the optimal tree is at
least as good and this gain is non-negative up to sampling noise (it is the
baseline's regret against the learned optimum, not the optimum's own regret,
which would need the unknown oracle value).
Source code in src/ergodic/inference/explain/_policy.py
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
PyMCClassifier
¶
Bases: PyMCLearner
A Bayesian classification learner over a PyMC model.
Name the probability node "p" (the per-row P(class=1)). Exposes
predict_proba and predict_proba_draws, so it fills a propensity or a
mediator slot and drives the posterior path there.
Source code in src/ergodic/inference/learners/_pymc.py
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |
PyMCLearner
¶
A Bayesian regression learner over a PyMC model.
Implement :meth:build to return a pymc.Model. predict is the
posterior predictive mean and predict_draws the (S, n) posterior of the
mean node, so the learner drops into a regression slot and also drives the
posterior path.
Source code in src/ergodic/inference/learners/_pymc.py
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | |
build(X, y)
¶
Return a pymc.Model. Subclasses implement this.
Source code in src/ergodic/inference/learners/_pymc.py
66 67 68 | |
RLearner
¶
Bases: _MetaLearnerBase
The R-learner: residualize the outcome and treatment, then fit the CATE.
Robinson's residual-on-residual moment with a heterogeneous final model, weighted by the squared treatment residual. The ATE is the average of the fitted CATE, with a bootstrap standard error.
Source code in src/ergodic/inference/estimators/_meta.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
Refutation
dataclass
¶
The result of one refutation check.
estimand_value is the effect on the real data. refuted_value is the
mean effect over the perturbed runs, with refuted_std their spread.
passed is the convenience verdict (the rule is in the method's section of
:func:refute_effect); read the numbers, not just the flag. p_value is
the permutation tail for the placebo check and None for the others.
Source code in src/ergodic/inference/_refute.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
SelectionReport
dataclass
¶
The ranked CATE model-selection result with the chosen and aggregated models.
ranking is the candidates sorted by ascending DR tau-risk (best first).
selected is the fitted :class:~ergodic.inference.CATEResult of the lowest
risk; selected_method names it. aggregate is the Q-aggregation ensemble
result and weights its convex weights (aligned to aggregate_methods),
with aggregate_tau_risk the ensemble's DR tau-risk.
Source code in src/ergodic/inference/_select.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
best_single_tau_risk
property
¶
The DR tau-risk of the best single candidate (the first in the ranking).
frame()
¶
A method-to-risk mapping (the aggregate included), handy for tables/tests.
Source code in src/ergodic/inference/_select.py
90 91 92 93 94 | |
risk(method)
¶
The DR tau-risk of a candidate by name (raises when it was not scored).
Source code in src/ergodic/inference/_select.py
78 79 80 81 82 83 | |
Slot
dataclass
¶
A named nuisance position with the learning task it requires.
Source code in src/ergodic/inference/_estimator.py
65 66 67 68 69 70 | |
Stratification
¶
Bases: _AdjustmentBase
Propensity stratification: bin on the propensity and difference within strata.
The estimate is the stratum-size-weighted (ATE) or treated-size-weighted (ATT) average of the within-stratum mean difference. Standard error by bootstrap.
Source code in src/ergodic/inference/estimators/_weighting.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
SyntheticControlEffect
dataclass
¶
Bases: EffectEstimate
A synthetic-control fit: the weights, the two paths, and placebo inference.
observed and synthetic run over periods, and gaps is their
difference. pre_rmspe is the tracking error before at, the fit
diagnostic to judge before trusting the effect. p_value ranks the
treated unit's post-to-pre RMSPE ratio among the donor placebos, and
placebo_gaps holds each donor's post-period placebo gaps. There is no
sampling story for one treated unit, so std_error stays None on
purpose and interval is a placebo band instead.
Source code in src/ergodic/inference/estimators/_synthetic_control.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
gaps
property
¶
Observed minus synthetic, one value per period.
interval(level=0.95)
¶
A placebo-permutation band around the estimate, not a sampling CI.
The half width is the level quantile of the absolute placebo
post-gap means: the gap a unit with no treatment can show. An estimate
whose band excludes zero clears the placebo bar at that level.
Source code in src/ergodic/inference/estimators/_synthetic_control.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
Task
¶
Bases: Enum
What a nuisance slot asks of its learner.
Source code in src/ergodic/inference/_estimator.py
55 56 57 58 59 60 61 62 | |
TwoStageLeastSquares
¶
Bases: _IVBase
Two-stage least squares. OLS in both stages, robust standard errors.
The first stage regresses the treatment on the instruments and any conditioning covariates; the second regresses the outcome on the fitted treatment. Reports the local average treatment effect (LATE).
Source code in src/ergodic/inference/estimators/_iv.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
TwoStageRegression
¶
Bases: _FrontdoorBase
Estimate the front-door functional from a mediator model and an outcome model.
For binary treatment A and binary mediator M: a classifier gives
P(M | A), a regression gives E[Y | M, A], and the effect is the
front-door sum E[Y | do(A=1)] - E[Y | do(A=0)]. Continuous and multi-level
mediators come later.
Source code in src/ergodic/inference/estimators/_frontdoor.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | |
VariableImportance
dataclass
¶
Depth-weighted, normalized split frequencies: a driver signal over covariates.
names are the covariate columns and values the matching importances, which
sum to one (or are all zero for a forest that never split). A split at depth d
contributes 1 / (d + 1), so shallow splits, the ones that move the effect most,
weigh more, following the GRF variable-importance convention.
Source code in src/ergodic/inference/estimators/_forest.py
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | |
top(k=None)
¶
The covariates by descending importance (all of them when k is None).
Source code in src/ergodic/inference/estimators/_forest.py
337 338 339 340 341 | |
Wald
¶
Bases: TwoStageLeastSquares
The Wald ratio: two-stage least squares with a single instrument.
Source code in src/ergodic/inference/estimators/_iv.py
97 98 99 100 101 102 103 104 105 106 107 108 109 | |
XLearner
¶
Bases: _MetaLearnerBase
The X-learner: per-arm outcome models, imputed effects, propensity-weighted.
Fit one outcome model per arm, impute each unit's effect against the other arm's model, regress those imputed effects per arm, and combine the two with the propensity. The ATE is the average of the fitted CATE, with a bootstrap standard error.
Source code in src/ergodic/inference/estimators/_meta.py
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
candidates(identification)
¶
Every estimator that consumes the identified family, default first.
Source code in src/ergodic/inference/_registry.py
207 208 209 | |
cate(graph, data, treatment, outcome, *, method='auto', learner=None, treated=None, random_state=None, n_boot=1000, uncertainty=True, **kwargs)
¶
Identify the effect of treatment on outcome, then estimate the CATE.
Identification-gated exactly like :func:estimate_effect: identify the effect,
select a CATE-capable estimator for the family, fit it, and return a
:class:~ergodic.inference.CATEResult. method names the meta-learner
("dr_learner" by default, the doubly-robust choice that shares the
explainability score helper; also "r_learner", "x_learner",
"t_learner", "s_learner", "dml"). learner fills the nuisance
slots (an alias, a learner, or a slot mapping). The result carries per-unit
uncertainty (a posterior with a Bayesian slot, the bootstrap otherwise, the
analytic standard error for the linear DR final) and the cached doubly-robust
scores, so :meth:~ergodic.inference.CATEResult.explain runs the Tier-B
characterization. Pass uncertainty=False to skip that step for a fast
point-only result (explain still works off the cached scores).
Source code in src/ergodic/inference/_estimate.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
did(data, outcome, treatment, *, controls=(), comparison='never_treated', strategy='auto', n_boot=1000, random_state=None)
¶
Difference-in-differences: the average effect of adoption on the treated (ATT).
The panel is read through adoption timing: a unit's adoption period is the first period its treatment equals 1, units that never show a 1 are never treated, and the treatment must be absorbing (never switching from 1 back to 0 within a unit). Periods compare by their position in the sorted period sequence, so integer and datetime periods both work.
Flavors. strategy="canonical" needs a single adoption period g: each
unit's outcome change from its pre-period mean (periods before g) to its
post-period mean (periods at or after g), treated minus comparison. Units
lacking a pre or a post observation are dropped with a warning and counted
in diagnostics["n_dropped_units"]. Controls enter through a unit-level
regression of that change on the treated flag and the unit's pre-period
control means. strategy="group_time" handles staggered adoption in the
style of Callaway and Sant'Anna: one ATT(g, t) per adoption cohort and
period, each differenced against the period just before adoption, then
aggregated over post periods with cohort-size weights; the cells land in
by_group_time. strategy="twfe" is the static two-way fixed-effects
regression (iterated within transform, so unbalanced panels work). Under
staggered adoption with effects that change over time it is a weighted mix
of cell effects with possibly negative weights (Goodman-Bacon), so prefer
group_time there. strategy="auto" picks canonical for one adoption
period and group_time for several, never twfe.
Comparison pools. comparison="never_treated" uses units that never
adopt and raises when none exist. comparison="not_yet_treated" also
admits units that adopt only after the periods a cell reads, the pool a
panel without never-treated units needs. In the canonical flavor the two
pools coincide because no later adopters exist, and twfe ignores the pool.
Inference is a unit bootstrap: resample units with replacement, recompute,
and keep n_boot draws (a failed resample is skipped, with a budget of
five attempts per draw). The result is kind="bootstrap"; the canonical
no-controls fit also stores the two-sample analytic standard error in
diagnostics["analytic_se"]. The parallel-trends and no-anticipation
assumptions ride along in design.
Raises ValueError for an unknown column, a missing value in the outcome, treatment, or controls, a treatment not coded 0/1 or boolean, a non-absorbing treatment, no treated units, or an empty comparison pool.
Source code in src/ergodic/inference/estimators/_did.py
746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 | |
dr_scores(identification, data, *, learners=None, folds=5, clip=0.001, treated=None, random_state=None)
¶
Cross-fit doubly-robust scores for an adjustment-identified effect.
learners is an alias, a learner, or a slot mapping over outcome and
propensity (the same surface every estimator takes); the defaults are the
native :class:OLS (or logistic for a binary outcome) and logistic propensity.
folds sets the cross-fit count. Returns a :class:DRScores carrying the
signal and the out-of-fold nuisances.
Source code in src/ergodic/inference/_scores.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
estimate(identification, data, *, strategy=None, learner=None, target=None, treated=None, random_state=None, n_boot=1000, **kwargs)
¶
Estimate an identified effect over tabular data.
strategy names the estimator (else the family default). learner is an
alias, a learner, or a slot-to-learner mapping (else the slot defaults).
treated names the treatment level that codes to 1 for a two-level
treatment (else level order decides, with a warning when the levels are not
0/1). Extra keyword arguments pass to the estimator (per_arm, scheme,
clip).
Source code in src/ergodic/inference/_estimate.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | |
estimate_effect(graph, data, treatment, outcome, *, strategy=None, learner=None, target=None, treated=None, random_state=None, n_boot=1000, **kwargs)
¶
Identify the effect of treatment on outcome in graph, then estimate it.
treated names the treatment level that codes to 1 (see :func:estimate).
Source code in src/ergodic/inference/_estimate.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
estimate_mediation(graph, data, treatment, outcome, mediators, *, learner=None, treated=None, random_state=None, n_boot=1000)
¶
Identify and estimate a mediation decomposition by g-computation.
Identifies the decomposition with
:func:~ergodic.identification.identify_mediation (raising if the natural
direct and indirect effects are not identifiable), then estimates NDE, NIE, the
total effect, and the controlled direct effect at each mediator level by
g-computation: fit mu(x, m, w) = E[Y | x, m, w] and p(m | x, w), plug
them into the mediation formula, and average over the empirical covariates.
For a binary treatment and a discrete (binary or low-cardinality) mediator,
with a continuous or binary outcome. learner is an alias, a learner, or a
{"outcome": ..., "mediator": ...} mapping (the mediator slot needs a
classifier). treated names the treatment level coded to 1. The standard
errors are a nonparametric bootstrap over the rows at random_state.
A continuous mediator raises (the density / multiply-robust path is not built);
a non-binary treatment raises. Returns a :class:MediationEstimate with a
:class:~ergodic.inference.Design record naming the (untestable) cross-world
assumption.
Source code in src/ergodic/inference/estimators/_mediation.py
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 | |
event_study(data, outcome, treatment, *, controls=(), comparison='never_treated', leads=None, lags=None, n_boot=1000, random_state=None)
¶
The effect by event time: leads as the placebo check, lags as the dynamic path.
Runs the group-time machinery of :func:did and aggregates the ATT(g, t)
cells that share an event time, the position of the period minus the
position of the adoption period, weighting cohorts by size. Event time -1
is the reference: every cell differences against the period just before
adoption, so the effect there is zero by construction and it is omitted.
leads and lags bound the reported window (leads=2 keeps event
times from -2, lags=3 up to 3); None keeps everything available.
The headline value is the overall post-adoption ATT, the same aggregate
:func:did reports under strategy="group_time", and by_event_time
carries one :class:EventTimeEffect per event time with a bootstrap
standard error and 95% percentile interval from the same unit bootstrap
that drives the headline draws. A lead whose interval sits away from zero
is evidence against parallel trends.
Validation and the comparison pools match :func:did.
Source code in src/ergodic/inference/estimators/_did.py
826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 | |
interrupted_time_series(data, outcome, *, at, seasonality=None, hac_lags=None)
¶
Fit an interrupted time series for outcome with a break at at.
The break index is the first row whose time value is at least at; both
sides need at least 3 points. The design matrix over row positions
t = 0..T-1 is [1, t, post, post * (t - t0)], plus, with
seasonality=m (between 2 and T // 2), Fourier pairs
sin/cos(2 pi k t / m) for k = 1..min(3, m // 2). OLS fits it, and
the coefficient covariance is Newey-West with Bartlett weights over
hac_lags lags, default floor(0.75 * T ** (1/3)).
level_change is the post coefficient and slope_change the
interaction. The headline value is the ATT, the mean post-period gap
between the fit and the pre-trend projection, which equals
level_change + slope_change * mean(t - t0 | post); its standard error
comes from the matching linear combination of the HAC covariance. The
counterfactual path zeroes the break terms but keeps the trend and
seasonal terms, all at the fitted coefficients.
A NaN outcome raises, as does a break too close to either end.
Source code in src/ergodic/inference/estimators/_its.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | |
policy_tree(result_or_scores, covariates=None, *, depth=2)
¶
Fit a shallow doubly-robust optimal-policy tree of depth depth.
result_or_scores is a :class:~ergodic.inference.CATEResult (its cached
cross-fit scores are read) or a :class:~ergodic.inference._scores.DRScores
directly. covariates names the split covariates (a name, a list of names, or
None for every covariate carried by the scores). The tree maximizes the
doubly-robust welfare W(pi) = mean[(2 pi(X) - 1) Gamma_i]; at depth <= 2
the search is exhaustive over thresholds and so globally optimal, and a deeper
tree uses a greedy recursive best-split (documented, not guaranteed optimal).
Returns a frozen :class:PolicyTree carrying the rule, the estimated policy
value with a standard error from the per-unit influence terms, and the regret
against treating everyone / no one.
Source code in src/ergodic/inference/explain/_policy.py
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 | |
refute_effect(graph, data, treatment, outcome, *, method='placebo_treatment', strategy=None, learner=None, treated=None, estimand_value=None, n_simulations=100, subset_fraction=0.8, random_state=None, **kwargs)
¶
Stress-test the effect of treatment on outcome under a perturbation.
Runs the same identify-then-estimate path as :func:estimate_effect, so pass
the same strategy, learner, and treated you used for the estimate.
estimand_value skips re-computing the effect on the real data when you
already have it.
The three methods:
"placebo_treatment"shuffles the treatment column, breaking its link to everything, and re-estimates. The placebo effect should sit near zero.passedwhen the mean placebo effect is under 20 percent of the real one;p_valueis the share of placebo runs whose effect matches or beats the real one in size."random_common_cause"adds an independent random covariate as a parent of both treatment and outcome and re-estimates. The effect should not move.passedwhen the mean stays within 10 percent of the original."data_subset"re-estimates on a randomsubset_fractionof the rows. The effect should hold.passedwhen the original sits within two standard deviations of the subset spread.
Flexible or cross-fit estimators make each run a full fit, so lower
n_simulations (or pick a faster strategy) when the estimator is heavy.
Source code in src/ergodic/inference/_refute.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | |
select_cate(graph, data, treatment, outcome, *, candidates=None, learner=None, treated=None, random_state=None, n_boot=1000, folds=5, nu=0.5, q_iters=200)
¶
Fit and rank candidate CATE learners by the doubly-robust tau-risk.
Identifies the effect (the same back-door gate as :func:~ergodic.inference.cate),
fits each candidate in candidates (default the Phase-1 registered meta-learners
dr_learner/r_learner/x_learner/t_learner) through the cate
facade, and scores each against one shared cross-fit AIPW signal Gamma by the
DR tau-risk mean[(Gamma - tau_hat)^2]. Returns a :class:SelectionReport with
the ranked candidates (best risk first), the selected fitted
:class:~ergodic.inference.CATEResult, and a Q-aggregation convex-ensemble result.
learner fills every candidate's nuisance slots; nu is the Q-aggregation
mix (the penalty pulling weight toward lower-risk models). The shared Gamma is
computed once with :func:~ergodic.inference.dr_scores, so the comparison is fair
across candidates and each candidate's own out-of-fold tau_hat keeps the
ranking honest.
Source code in src/ergodic/inference/_select.py
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | |
synthetic_control(data, outcome, *, treatment=None, treated=None, at=None, donors=None)
¶
Fit a synthetic control for one treated unit in a panel.
Name the treated unit in exactly one of two ways. Pass treatment, an
absorbing 0/1 column (once a unit turns 1 it stays 1) that marks exactly
one unit as treated; its first treated period becomes at. Or pass
treated and at directly. donors defaults to every other unit.
The donor weights minimize the squared pre-period tracking error over the
simplex (nonnegative, summing to one) by projected gradient descent with
the Duchi projection; the run is deterministic. The headline value is the
ATT, the mean of observed - synthetic over the post periods
(t >= at), and pre_rmspe reports the fit it rests on.
Inference is by placebo permutation, with the treated unit excluded from
every placebo run: each donor is treated as pseudo-treated, fit on the
remaining donors, and scored by its post-to-pre RMSPE ratio (skipped when
its pre RMSPE falls below 1e-12). p_value is
(1 + count(ratio_j >= ratio_treated)) / (1 + n_placebos). There is no
sampling standard error for one treated unit, so std_error stays
None and interval reads the placebo gaps instead.
Validation: at must be one of the panel's periods with at least 2 pre
periods and 1 post period; the treated unit must be observed at every
period; donors missing any period are dropped with a warning and at least
2 complete donors must remain; a NaN outcome among the used rows raises.
Source code in src/ergodic/inference/estimators/_synthetic_control.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 | |
Per-unit effects (CATE/ITE) and explainability¶
The cate facade and the uniform per-unit result. cate(graph, data, X, Y) is identification-gated like estimate_effect and returns a CATEResult; SCM.ite returns an ITEResult. Both share the PerUnitEffect surface (.values, .interval, .ate, __array__). CATEResult.explain returns a Heterogeneity record for Tier-B characterization (BLP, GATES, CLAN, the calibration test, RATE).
ergodic.cate(graph, data, treatment, outcome, *, method='auto', learner=None, treated=None, random_state=None, n_boot=1000, uncertainty=True, **kwargs)
¶
Identify the effect of treatment on outcome, then estimate the CATE.
Identification-gated exactly like :func:estimate_effect: identify the effect,
select a CATE-capable estimator for the family, fit it, and return a
:class:~ergodic.inference.CATEResult. method names the meta-learner
("dr_learner" by default, the doubly-robust choice that shares the
explainability score helper; also "r_learner", "x_learner",
"t_learner", "s_learner", "dml"). learner fills the nuisance
slots (an alias, a learner, or a slot mapping). The result carries per-unit
uncertainty (a posterior with a Bayesian slot, the bootstrap otherwise, the
analytic standard error for the linear DR final) and the cached doubly-robust
scores, so :meth:~ergodic.inference.CATEResult.explain runs the Tier-B
characterization. Pass uncertainty=False to skip that step for a fast
point-only result (explain still works off the cached scores).
Source code in src/ergodic/inference/_estimate.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
ergodic.CATEResult
dataclass
¶
Bases: PerUnitEffect
Conditional average treatment effects, with an explainability hook.
A conditional average over the unobserved noise (the inference-pillar object).
Carries the :class:~ergodic.identification.Identification it was fit under and
the cached doubly-robust scores + covariate design, so :meth:explain projects
honest summaries (BLP, GATES, CLAN, the calibration test, RATE) off the same
orthogonal signal.
Source code in src/ergodic/inference/_perunit.py
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | |
explain()
¶
Tier-B characterization of the heterogeneity, on the doubly-robust scores.
Builds a :class:~ergodic.inference.explain.Heterogeneity record whose
methods (blp, gates, clan, calibration_test, rate) all
consume the cached cross-fit scores, the covariate design, and the fitted
per-unit tau_hat. Needs the scores; raises when they were not cached
(an estimator that does not build them, or a result without them).
Source code in src/ergodic/inference/_perunit.py
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 | |
ergodic.ITEResult
dataclass
¶
Bases: PerUnitEffect
Individual treatment effects from a structural model (the twin network).
The individual's own counterfactual contrast (the SCM-pillar object), not a
conditional average. Carries the factual outcome column, the contrast framing
(levels= or shift=), and a structural provenance note.
:meth:explain is deferred: the Tier-B machinery is defined over the CATE's
doubly-robust scores, and the inference-valid descriptive regression of abducted
ITEs is a later phase, so it raises with a pointer rather than faking inference.
Source code in src/ergodic/inference/_perunit.py
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 | |
factual
property
¶
The per-unit factual outcome the contrast was taken against, when carried.
explain()
¶
Deferred in phase 1: the Tier-B tools need the CATE's doubly-robust scores.
Source code in src/ergodic/inference/_perunit.py
306 307 308 309 310 311 312 313 | |
ergodic.PerUnitEffect
dataclass
¶
Per-unit treatment effects with their uncertainty.
_values is the per-unit point vector (n,), ordered to the input rows.
Either _std_error (per-unit analytic SEs) or _draws (S, n) (bootstrap
or posterior) carries the uncertainty, and the accessors read whichever is
present. index are explicit row ids; estimand is set by the subclass.
Source code in src/ergodic/inference/_perunit.py
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | |
index
property
¶
The row ids for the units, positional arange(n) when none were given.
shape
property
¶
The shape of the per-unit point vector, (n,).
std_error
property
¶
Per-unit standard errors, or the per-unit draw standard deviations.
values
property
¶
The per-unit point estimates, (n,), ordered to the input rows.
When only draws are present, the posterior or bootstrap mean per unit.
__array__(dtype=None)
¶
The per-unit point vector, so np.asarray(result) and arithmetic work.
Source code in src/ergodic/inference/_perunit.py
80 81 82 83 | |
__getitem__(item)
¶
Index into the per-unit point vector (so result[i] is the unit's effect).
Source code in src/ergodic/inference/_perunit.py
90 91 92 | |
ate()
¶
The population summary: the average per-unit effect, as an EffectEstimate.
The bridge back to the scalar family. With draws, the per-unit draws average to a draw vector of the mean effect (so the population interval reflects the joint uncertainty). With per-unit analytic SEs, the point is the mean and the standard error combines them assuming independence. With neither, a point.
Source code in src/ergodic/inference/_perunit.py
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | |
draws()
¶
The (S, n) bootstrap or posterior draws, or None for a point result.
Source code in src/ergodic/inference/_perunit.py
156 157 158 159 160 | |
interval(level=0.9)
¶
Per-unit confidence or credible intervals, (lo[n], hi[n]) at level.
Draw quantiles when draws are present, else the normal interval from the per-unit point and standard error. Raises when there is no uncertainty.
Source code in src/ergodic/inference/_perunit.py
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
to_frame()
¶
A per-unit table (index, effect, and lo/hi/se when available). Pandas only here.
Source code in src/ergodic/inference/_perunit.py
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | |
ergodic.inference.Heterogeneity
dataclass
¶
Tier-B heterogeneity characterization on shared doubly-robust scores.
Carries the cross-fit :class:~ergodic.inference._scores.DRScores and the fitted
per-unit tau_hat. The honesty register: the rankings and proxies are fixed
(computed cross-fit), so the orthogonal summaries below inherit valid inference
while the CATE point predictions themselves never carry a confidence interval.
Source code in src/ergodic/inference/explain/_record.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
blp(*, level=0.9)
¶
Best Linear Projection of the CATE on the covariates (HC3 inference).
Source code in src/ergodic/inference/explain/_record.py
55 56 57 58 59 60 61 62 | |
calibration_test()
¶
The omnibus calibration / heterogeneity test (one-sided HTE p-value).
Source code in src/ergodic/inference/explain/_record.py
78 79 80 | |
clan(*, n_groups=5, level=0.9)
¶
Covariate-mean differences between the most- and least-affected groups.
Source code in src/ergodic/inference/explain/_record.py
68 69 70 71 72 73 74 75 76 | |
gates(*, n_groups=5, level=0.9)
¶
Sorted Group ATEs across n_groups predicted-effect quantiles.
Source code in src/ergodic/inference/explain/_record.py
64 65 66 | |
policy_tree(covariates=None, *, depth=2)
¶
The depth-limited, DR-welfare-optimal policy tree off these scores.
A convenience that forwards the shared doubly-robust scores to
:func:~ergodic.inference.policy_tree. covariates selects the split
columns by name (None uses all of them). The "act" companion to
:meth:rate: where rate scores a ranking, this returns an interpretable
treat-or-not rule and its estimated policy value.
Source code in src/ergodic/inference/explain/_record.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
rate(metric='autoc', *, n_boot=200, random_state=None)
¶
The Rank-Weighted ATE of the tau_hat ranking: "autoc" or "qini".
Source code in src/ergodic/inference/explain/_record.py
82 83 84 85 86 87 88 89 90 91 92 | |
Causal forest, policy trees, and CATE model selection¶
The honest causal forest (GRF) is its own CATE learner, reachable through the cate facade (method="causal_forest" or "grf") or directly as CausalForest; it returns a CATEResult with native per-unit infinitesimal-jackknife intervals, and CausalForest(...).fit(...).variable_importance() returns a VariableImportance driver signal. policy_tree (also CATEResult.explain().policy_tree(...)) learns an interpretable, depth-limited, doubly-robust-optimal treat/don't PolicyTree. select_cate ranks candidate CATE learners by the doubly-robust tau-risk and returns a SelectionReport with the selected result and a Q-aggregation ensemble.
ergodic.CausalForest
¶
Bases: _MetaLearnerBase
An honest causal forest (GRF) for the conditional average treatment effect.
The forest is the CATE learner. The outcome and propensity nuisance slots
take any learner (they residualize Y and W on the covariates before the
forest grows); the forest machinery, honest splitting and the kernel readout, is
native. The ATE is the average per-unit effect, and cate returns a
:class:~ergodic.inference.CATEResult with per-unit infinitesimal-jackknife
standard errors and intervals.
Hyperparameters: n_trees (forest size), min_leaf (the minimum estimate-half
rows per leaf), max_depth, honest (split/estimate sample-splitting, on by
default), subsample (the per-tree subsample fraction), and random_state.
Binary treatment in this phase; a continuous treatment via the same local moment
is deferred and raises.
Source code in src/ergodic/inference/estimators/_forest.py
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 | |
cate(data=None, *, uncertainty=True)
¶
Per-unit CATE with infinitesimal-jackknife standard errors and intervals.
Overrides the base meta-learner cate (which would bootstrap-refit the
whole forest). The point is the forest-kernel local effect; the per-unit
variance is the infinitesimal jackknife over the trees (the
bootstrap-of-little-bags grouping), valid because the forest is honest and
subsampled. The result carries the cached doubly-robust scores for
:meth:~ergodic.inference.CATEResult.explain.
uncertainty=False returns the point effects with kind="point" and no
standard error, skipping the infinitesimal-jackknife pass over the trees.
Source code in src/ergodic/inference/estimators/_forest.py
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 | |
variable_importance()
¶
Depth-weighted, normalized split frequencies over the covariates.
A driver signal: which covariates the forest splits on, weighted toward the
shallow splits that move the effect most. Available after fit.
Source code in src/ergodic/inference/estimators/_forest.py
626 627 628 629 630 631 632 633 | |
ergodic.inference.VariableImportance
dataclass
¶
Depth-weighted, normalized split frequencies: a driver signal over covariates.
names are the covariate columns and values the matching importances, which
sum to one (or are all zero for a forest that never split). A split at depth d
contributes 1 / (d + 1), so shallow splits, the ones that move the effect most,
weigh more, following the GRF variable-importance convention.
Source code in src/ergodic/inference/estimators/_forest.py
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | |
top(k=None)
¶
The covariates by descending importance (all of them when k is None).
Source code in src/ergodic/inference/estimators/_forest.py
337 338 339 340 341 | |
ergodic.policy_tree(result_or_scores, covariates=None, *, depth=2)
¶
Fit a shallow doubly-robust optimal-policy tree of depth depth.
result_or_scores is a :class:~ergodic.inference.CATEResult (its cached
cross-fit scores are read) or a :class:~ergodic.inference._scores.DRScores
directly. covariates names the split covariates (a name, a list of names, or
None for every covariate carried by the scores). The tree maximizes the
doubly-robust welfare W(pi) = mean[(2 pi(X) - 1) Gamma_i]; at depth <= 2
the search is exhaustive over thresholds and so globally optimal, and a deeper
tree uses a greedy recursive best-split (documented, not guaranteed optimal).
Returns a frozen :class:PolicyTree carrying the rule, the estimated policy
value with a standard error from the per-unit influence terms, and the regret
against treating everyone / no one.
Source code in src/ergodic/inference/explain/_policy.py
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 | |
ergodic.PolicyTree
dataclass
¶
A shallow doubly-robust optimal-policy tree with its value and regret.
root is the learned tree over the named covariates; :meth:predict and
:meth:assign evaluate it. value/std_error are the estimated policy
value W(pi_hat) and its standard error from the per-unit influence terms;
treat_all_value and treat_none_value are the two trivial baselines, and
:meth:regret is the value gap against either. depth is the requested depth
and exhaustive records whether the globally optimal search was used.
Source code in src/ergodic/inference/explain/_policy.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
assign()
¶
The fitted policy's action for each unit it was learned on.
Source code in src/ergodic/inference/explain/_policy.py
123 124 125 | |
describe()
¶
A readable if-else rendering of the tree (the interpretable rule).
Source code in src/ergodic/inference/explain/_policy.py
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
interval(level=0.9)
¶
A normal confidence interval for the policy value at level.
Source code in src/ergodic/inference/explain/_policy.py
127 128 129 130 131 132 133 134 | |
leaves()
¶
The leaf nodes, left to right (each carries its action, count, and value).
Source code in src/ergodic/inference/explain/_policy.py
159 160 161 162 163 164 165 166 167 168 169 170 171 172 | |
predict(covariates)
¶
The per-row action (0/1) for a covariate matrix aligned to covariates.
Source code in src/ergodic/inference/explain/_policy.py
112 113 114 115 116 117 118 119 120 121 | |
regret(baseline='treat_all')
¶
The learned policy's welfare GAIN over a trivial baseline (the baseline's regret).
baseline is "treat_all", "treat_none", or "best_trivial" (the
better of the two). Returns value - baseline_value: how much welfare the
learned rule adds over treating everyone / no one. The trivial constant
policies are depth-0 leaves inside the tree's class, so the optimal tree is at
least as good and this gain is non-negative up to sampling noise (it is the
baseline's regret against the learned optimum, not the optimum's own regret,
which would need the unknown oracle value).
Source code in src/ergodic/inference/explain/_policy.py
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
ergodic.select_cate(graph, data, treatment, outcome, *, candidates=None, learner=None, treated=None, random_state=None, n_boot=1000, folds=5, nu=0.5, q_iters=200)
¶
Fit and rank candidate CATE learners by the doubly-robust tau-risk.
Identifies the effect (the same back-door gate as :func:~ergodic.inference.cate),
fits each candidate in candidates (default the Phase-1 registered meta-learners
dr_learner/r_learner/x_learner/t_learner) through the cate
facade, and scores each against one shared cross-fit AIPW signal Gamma by the
DR tau-risk mean[(Gamma - tau_hat)^2]. Returns a :class:SelectionReport with
the ranked candidates (best risk first), the selected fitted
:class:~ergodic.inference.CATEResult, and a Q-aggregation convex-ensemble result.
learner fills every candidate's nuisance slots; nu is the Q-aggregation
mix (the penalty pulling weight toward lower-risk models). The shared Gamma is
computed once with :func:~ergodic.inference.dr_scores, so the comparison is fair
across candidates and each candidate's own out-of-fold tau_hat keeps the
ranking honest.
Source code in src/ergodic/inference/_select.py
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | |
ergodic.SelectionReport
dataclass
¶
The ranked CATE model-selection result with the chosen and aggregated models.
ranking is the candidates sorted by ascending DR tau-risk (best first).
selected is the fitted :class:~ergodic.inference.CATEResult of the lowest
risk; selected_method names it. aggregate is the Q-aggregation ensemble
result and weights its convex weights (aligned to aggregate_methods),
with aggregate_tau_risk the ensemble's DR tau-risk.
Source code in src/ergodic/inference/_select.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
best_single_tau_risk
property
¶
The DR tau-risk of the best single candidate (the first in the ranking).
frame()
¶
A method-to-risk mapping (the aggregate included), handy for tables/tests.
Source code in src/ergodic/inference/_select.py
90 91 92 93 94 | |
risk(method)
¶
The DR tau-risk of a candidate by name (raises when it was not scored).
Source code in src/ergodic/inference/_select.py
78 79 80 81 82 83 | |
Process intelligence¶
Event-log mining: the map, the causal case table, the model layer, and decision-point analysis.
ergodic.process
¶
Process mining and intelligence: from event logs to causal questions.
The descriptive layer recovers the daily workflow over an
:class:~ergodic.data.EventLog: :func:process_map builds the
directly-follows map as a value object (frequencies, gap statistics, and a
simplify that records what it dropped), :func:performance reports cycle
times and, when a lifecycle column exists, the waiting and service split,
:func:variant_table ranks the trace variants, and the filter vocabulary
(:func:filter_time, :func:filter_variants, :func:filter_endpoints,
:func:filter_activities, :func:filter_follows, :func:filter_performance)
composes, with each filter saying whether it drops events or cases.
The causal layer is the point. :func:case_table encodes the log into one row
per case with timestamp-gated features, the named process confounders
(workload, calendar), and an auto-built :class:~ergodic.knowledge.DomainKnowledge
whose tiers forbid post-treatment adjustment mechanically; its output feeds
:func:~ergodic.discovery.discover, :func:~ergodic.identification.identify_effect,
and :func:~ergodic.inference.estimate_effect unchanged. :func:kpi_panel
aggregates the log into a :class:~ergodic.data.PanelData (or a single
series), so a process change becomes a natural experiment for
:func:~ergodic.inference.did, :func:~ergodic.inference.event_study,
:func:~ergodic.inference.synthetic_control, and
:func:~ergodic.inference.interrupted_time_series.
The model layer goes past the map. :func:discover_model is the inductive
miner: it cuts the log into a block-structured :class:ProcessTree (build one
by hand with :func:seq, :func:xor, :func:par, :func:loop, and
:data:TAU) whose :meth:~ProcessTree.to_net gives a :class:WorkflowNet
that is sound by construction, with :func:is_sound checking any net for
real. :func:conformance replays the log against a model, token replay for
the fast fitness and optimal alignments for the per-variant deviations. And
:func:decision_points with :func:decision_table make the model causal: an
exclusive split is a decision, the branch taken is a treatment, the features
known at the decision moment are the honest adjusters, and positivity ships
as a first-class report instead of a footnote.
END = '__end__'
module-attribute
¶
Target of the synthetic arc out of each case's last activity.
START = '__start__'
module-attribute
¶
Source of the synthetic arc into each case's first activity.
TAU = ProcessTree()
module-attribute
¶
The silent step: a leaf that takes no time and leaves no event.
ActivityStats
dataclass
¶
Per-activity timing: event count, mean wait before it, mean service inside it.
count is how many events carry the activity. mean_wait is None
when no event of the activity has anything earlier in its case to wait
on; mean_service is None on complete-only logs, where one timestamp
per activity cannot split waiting from working, and for activities with
no matched start/complete pair.
Source code in src/ergodic/process/_performance.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
ArcStats
dataclass
¶
Traversal count and gap timing for one arc of the map.
mean_gap and median_gap are seconds between the two events. A
START or END arc has no second event, so both are NaN there (the one
shared math.nan object, which keeps identically built maps equal).
Source code in src/ergodic/process/_map.py
49 50 51 52 53 54 55 56 57 58 59 60 | |
CaseTable
dataclass
¶
An event log flattened to one row per case, ready for the causal stack.
data holds the covariates (sorted), the treatment, and the outcome, one
row per kept case in case order. knowledge carries the tier order
covariates, then treatment, then outcome. The drop counters are zero
outside the landmark modes.
Source code in src/ergodic/process/_case_table.py
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
ConformanceResult
dataclass
¶
How well a log fits a model, by token replay and by alignments.
fitness is the token-replay log fitness in Rozinat's form: the
missing/consumed and remaining/produced sums aggregate over all cases
before the two halves average, so frequent variants weigh more.
alignment_fitness is the case-weighted mean of the per-variant
alignment fitness. A case is fitting when its variant aligns at cost
zero; n_fitting and share_fitting count those. variants
holds one :class:VariantConformance per distinct trace, most frequent
first, ties by sequence.
Source code in src/ergodic/process/_conformance.py
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
DecisionPoint
dataclass
¶
One xor split of a process tree, with the branch each case took.
path locates the xor node in the :meth:ProcessTree.nodes convention.
The branch tuples line up positionally: branches[j] names the j-th
child (a single visible leaf by its label, any other child by its sorted
alphabet joined with +, a silent child as TAU), alphabets[j]
is its activity alphabet, and counts[j] and shares[j] say how many
assigned cases took it. A case is assigned the branch owning its first
event inside the split's combined alphabet; events in two or more branches
make it ambiguous (n_ambiguous, excluded from the counts). A case
touching no branch counts as the silent branch when the split has one and
is always reached; otherwise it lands in n_skipped, because the log
cannot tell a silent branch from a split never reached.
Source code in src/ergodic/process/_decisions.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
DecisionTable
dataclass
¶
A decision point encoded for the causal stack, one row per assigned case.
data holds the sorted covariates, the categorical branch treatment,
and the outcome, one row per unambiguously assigned case in case order.
knowledge carries the tier order covariates, then branch, then outcome.
point is the underlying :class:DecisionPoint, positivity the
coverage diagnostic, and n_dropped_ambiguous counts the cases excluded
for touching two or more branches.
Source code in src/ergodic/process/_decisions.py
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
PositivityReport
dataclass
¶
Per-branch coverage of a decision table: the positivity diagnostic.
counts and shares are per branch among the included cases, and
min_share is the smallest share. For a two-branch split,
overlap_share is the share of cases whose in-sample propensity (a
logistic regression of the branch on the covariate matrix) lies inside
[0.05, 0.95]; it is None for wider splits, and 1.0 when there are no
covariates, since the propensity is then the base rate for every case and
the covariates cannot starve either branch.
flagged is True when min_share falls below 0.05 (one branch holds
under five percent of the cases), any branch holds fewer than 10 cases
(too few for a stable branch mean), or overlap_share exists and falls
below 0.8 (for over a fifth of the cases the covariates nearly decide the
branch, so the comparison there rests on extrapolation). A flag is a
warning that the branches barely overlap, not a verdict that an estimate
is wrong.
Source code in src/ergodic/process/_decisions.py
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
ProcessMap
dataclass
¶
A directly-follows map: activities, timed arcs, and filter provenance.
activities lists the real activities, sorted. arcs maps each
(source, target) pair to its :class:ArcStats and iterates in sorted
key order; it holds one (START, a) arc per first activity and one
(b, END) arc per last, with per-case counts aggregated. A freshly
built map has dropped_arcs and dropped_share at zero; they record
what :meth:simplify has removed and accumulate over repeated calls.
Source code in src/ergodic/process/_map.py
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
frequency(a, b)
¶
How many times the arc a -> b was traversed, 0 when it is absent.
Source code in src/ergodic/process/_map.py
82 83 84 85 | |
simplify(keep=0.8)
¶
A thinner map keeping the busiest arcs that cover keep of the traversals.
keep must lie in (0, 1]. START and END arcs always stay, so every
case still enters and leaves the map. Real arcs are ranked by count,
heaviest first (ties broken by the sorted pair), and the smallest
prefix whose counts reach keep times the total real traversals
survives; the rest are dropped. Every activity stays, isolated ones
included.
Filtering is visible, never silent: dropped_arcs and
dropped_share on the returned map say how many arcs are gone and
what share of the original log's real traversals they carried, and
both accumulate across repeated calls.
Source code in src/ergodic/process/_map.py
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | |
to_dict()
¶
The map as plain data: the activities and arcs as lists, plus the counts.
Source code in src/ergodic/process/_map.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | |
ProcessModel
dataclass
¶
A mined process model and the provenance of everything removed.
tree is the block-structured model and net is its workflow net,
tree.to_net(), sound by construction. The rest records what the miner
filtered or dropped on the way (filtering is visible, never silent):
fallthroughs: paths, in :meth:ProcessTree.nodesconvention (child indices from the root), of the subtrees where no cut was valid and the miner fell through to the flower model, which accepts its activities in any order. Empty when every subtree came from a real cut.n_filtered_arcs: directly-follows edges removed by the infrequent filter, summed over all recursions. Zero whennoiseis 0.n_dropped_events: events discarded while projecting traces through an exclusive-choice cut, where each trace goes whole to one branch and its events outside that branch are dropped.dropped_share:n_dropped_eventsover the log's total events.noise: the filter level the model was mined with.
Source code in src/ergodic/process/_mine.py
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
ProcessPerformance
dataclass
¶
Case cycle times plus the per-activity timing table.
Durations are each case's last event minus its first, in seconds.
activities is sorted by activity name. lifecycle says whether a
waiting/service split was possible, and n_unmatched counts lifecycle
rows that found no partner.
Source code in src/ergodic/process/_performance.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | |
ProcessTree
dataclass
¶
One node of a process tree.
A leaf has no children and carries an activity label (or None,
which is the silent step :data:TAU). An operator node has op set,
no label, and at least two children; loop takes exactly two, the do
body and the redo body. The children of xor and par are stored
sorted by their activity alphabets (a silent child first), so equal models
are equal objects regardless of how they were assembled.
Source code in src/ergodic/process/_tree.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | |
is_leaf
property
¶
Whether this node is a leaf (an activity or the silent step).
is_tau
property
¶
Whether this node is the silent step.
activities()
¶
The activity labels in this subtree, sorted and unique.
Source code in src/ergodic/process/_tree.py
73 74 75 76 77 78 79 80 | |
nodes()
¶
Every node with its path from the root, depth first.
A path is the tuple of child indices leading to the node; the root's
path is ().
Source code in src/ergodic/process/_tree.py
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | |
subtree(path)
¶
The node at path, where each entry picks a child by index.
Source code in src/ergodic/process/_tree.py
98 99 100 101 102 103 104 105 | |
to_net()
¶
The workflow net of this tree, sound by construction.
Source code in src/ergodic/process/_tree.py
107 108 109 110 111 | |
Variant
dataclass
¶
One distinct activity sequence and its case statistics.
Source code in src/ergodic/process/_variants.py
25 26 27 28 29 30 31 32 33 | |
VariantConformance
dataclass
¶
Replay and alignment results for one distinct trace.
sequence is the trace and n_cases how many cases follow it. The
token-replay counters come next: missing tokens had to be added,
remaining were left behind, and consumed and produced count
every token the replay moved, the initial token and the final take from
the sink included. replay_fitness is Rozinat's half-and-half of the
two ratios. cost is the optimal alignment cost, worst_cost the
reference cost of deleting the whole trace and running the model's
cheapest visible path instead, and alignment_fitness is
1 - cost / worst_cost. alignment lists the moves: ("sync", a)
where trace and model agree, ("log", a) for an event the model cannot
mirror, ("model", a) for a required visible model step the trace
skipped. Silent model steps cost nothing and are omitted from the list.
Source code in src/ergodic/process/_conformance.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
WorkflowNet
dataclass
¶
A workflow net: places, labeled or silent transitions, and arcs.
transitions maps ids to labels as (id, label) pairs, with None
for a silent transition; ids and place names live in one namespace and
must not collide. Arcs connect a place to a transition or a transition to
a place, once each (these are weight-one nets). The constructor sorts the
fields into canonical order and validates the workflow-net shape: a single
source with no inputs, a single sink with no outputs, and every
node on some directed path from source to sink.
Source code in src/ergodic/process/_net.py
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | |
final_marking
property
¶
One token on the sink place.
initial_marking
property
¶
One token on the source place.
index()
¶
The pre/post maps for the firing rule; build once per algorithm run.
Source code in src/ergodic/process/_net.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
case_table(log, treatment, outcome='cycle_time', *, covariates='auto', when='anytime')
¶
Encode an event log as one row per case: covariates, treatment, outcome.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
log
|
EventLog
|
The event log to flatten. |
required |
treatment
|
str
|
An activity name (the treatment is whether it occurred, per
|
required |
outcome
|
str
|
|
'cycle_time'
|
covariates
|
str | Iterable[str]
|
|
'auto'
|
when
|
str | tuple[str, int | float]
|
|
'anytime'
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
CaseTable
|
class: |
CaseTable
|
in the order sorted covariates, treatment, outcome; one row per kept |
|
CaseTable
|
case, in case order), the tiered :class: |
|
CaseTable
|
and the drop counters of the landmark modes. |
Source code in src/ergodic/process/_case_table.py
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 | |
conformance(log, model, *, max_states=100000)
¶
Check log against model by token replay and by optimal alignments.
A mined :class:~ergodic.process.ProcessModel contributes its net, and a
process tree converts to its workflow net up front. Each distinct
variant replays and aligns once, then weights by its case count:
fitness aggregates the replay token sums over all cases (Rozinat's
form), alignment_fitness is the case-weighted mean of the per-variant
alignment fitness, and a case is fitting when its variant aligns at cost
zero. max_states caps every state search (the silent closures inside
replay, the alignment search, and the model's cheapest-run search); past
the cap a ValueError raises rather than returning a partial answer.
A model that cannot reach its final marking at all raises too, since no
alignment exists for it.
Source code in src/ergodic/process/_conformance.py
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 | |
decision_points(log, model)
¶
Locate every xor split of model and the branch each case of log took.
model is a process tree or a mined :class:~ergodic.process.ProcessModel
(its tree is read). Returns one :class:DecisionPoint per xor node, in
depth-first order. The tree must carry each activity label in exactly one
leaf (a mined tree does, by construction); a duplicated label raises, since
branch membership would then be ambiguous by design rather than by data.
Source code in src/ergodic/process/_decisions.py
280 281 282 283 284 285 286 287 288 289 290 291 | |
decision_table(log, model, *, point=None, outcome='remaining_time', covariates='auto')
¶
Encode one xor split of model as a causal table: covariates, branch, outcome.
The landmark is per case the time of its first event inside the split's
combined alphabet, the moment the case reveals its branch. Only cases
assigned exactly one branch enter the table; cases touching two or more
branches are dropped and counted in n_dropped_ambiguous. A split with
a silent (tau or empty-alphabet) branch raises: taking it leaves no event,
so the decision moment is unobservable without alignment-based replay.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
log
|
EventLog
|
The event log the tree describes. |
required |
model
|
ProcessTree | ProcessModel
|
A process tree with each activity in exactly one leaf, or a
mined :class: |
required |
point
|
tuple[int, ...] | DecisionPoint | None
|
The xor node to analyze: its path, a :class: |
None
|
outcome
|
str
|
|
'remaining_time'
|
covariates
|
str | Sequence[str]
|
|
'auto'
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
DecisionTable
|
class: |
DecisionTable
|
(columns in the order sorted covariates, |
|
DecisionTable
|
per kept case, in case order), the tiered |
|
DecisionTable
|
class: |
|
DecisionTable
|
class: |
Source code in src/ergodic/process/_decisions.py
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 | |
discover_model(log, *, noise=0.0)
¶
Mine a block-structured process model from log.
The miner works on the log's variants, weighting each distinct trace by
how many cases follow it, so a large log mines as fast as its variant
table. noise must lie in [0, 1): at 0 the log is taken literally,
and a larger value drops, at every recursion and before cut detection,
each directly-follows edge carrying less than noise times the
heaviest edge leaving the same activity. The returned
:class:ProcessModel carries the tree, its workflow net, and the
provenance counters.
Source code in src/ergodic/process/_mine.py
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
filter_activities(log, *, keep=None, mandatory=None, forbidden=None)
¶
Drops cases: by mandatory and forbidden activities; keep then drops events.
A case is dropped when it misses any mandatory activity or contains
any forbidden one, judged on its full trace. When keep is given,
the surviving cases then lose every event whose activity is not in it,
and a case left with no events vanishes. At least one argument is
required, and an activity name the log has never seen raises.
Source code in src/ergodic/process/_filters.py
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | |
filter_endpoints(log, *, start_in=None, end_in=None)
¶
Drops cases: keep cases that start and end in the given activity sets.
start_in constrains a trace's first activity and end_in its last;
a None set leaves that end unconstrained. The standard way to drop
incomplete cases.
Source code in src/ergodic/process/_filters.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | |
filter_follows(log, a, b, *, directly=True, within=None)
¶
Drops cases: keep cases where activity b follows activity a.
With directly (the default) b must be the very next event after
an a; otherwise any later event counts. within bounds the gap in
seconds from the a event to the b event, so a pair further apart
does not count. Both activities must be known to the log.
Source code in src/ergodic/process/_filters.py
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | |
filter_performance(log, *, min_duration=None, max_duration=None, min_events=None, max_events=None)
¶
Drops cases: by case duration in seconds (first to last event) and event count.
Bounds are inclusive and a None bound is open; give at least one.
Source code in src/ergodic/process/_filters.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | |
filter_time(log, *, start=None, end=None, mode='intersecting')
¶
Drops cases: keep cases meeting the window on event times; mode "trim" drops events.
start and end accept whatever compares to the log's stamps, a
datetime-like or a number, and a None bound is open. Three modes:
"contained" keeps cases whose every event falls inside [start, end],
"intersecting" (the default) keeps cases with at least one event inside,
and "trim" keeps only the events inside, so a case losing all its events
vanishes.
Source code in src/ergodic/process/_filters.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | |
filter_variants(log, *, top=None, min_cases=None)
¶
Drops cases: keep cases whose full activity sequence is a frequent variant.
Give exactly one selector. top keeps the top most frequent
variants (count descending, ties by sequence, the :func:variant_table
ranking); min_cases keeps every variant with at least that many cases.
Source code in src/ergodic/process/_filters.py
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | |
is_sound(net, *, max_states=100000)
¶
Whether net is sound in van der Aalst's classical sense.
Three conditions, all decided on the real state space. Option to complete: from every marking reachable from the initial marking, the final marking is still reachable. Proper completion: every reachable marking that puts a token on the sink is exactly the final marking, so no run finishes with tokens left behind. No dead transitions: every transition fires on some reachable path.
The check explores markings breadth-first from
:attr:~ergodic.process.WorkflowNet.initial_marking, recording which
marking fires which transition into which marking. Proper completion is
then read off the explored markings, dead transitions off the union of
fired transition ids, and option to complete by walking the recorded
state graph backward from the final marking: every explored marking must
reach it, which also settles whether the final marking is reachable at
all.
A sound net is bounded, so the exploration terminates. When the number
of distinct markings passes max_states, the net is either unbounded
(and so unsound) or genuinely this big, and the function raises a
ValueError rather than guess; raise the cap if the net really is
this big.
Source code in src/ergodic/process/_soundness.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
kpi_panel(log, period, *, unit=None, kpis=('cycle_time',))
¶
Aggregate an event log into per-period KPI rows for the panel estimators.
Each case belongs to the bucket of its first event and, when unit is
given, to its unit, read from a case-constant column of the log (a column
that varies within a case raises). Every aggregate in a row is over the
cases that start in that unit-period cell. A cell where no case starts is
simply absent; the panel designs handle the unbalanced grid.
period picks the bucketing. With datetime stamps pass "day",
"week", or "month"; the bucket labels are np.datetime64
values: the stamp's calendar day (datetime64[D]), the Monday of the
stamp's week (datetime64[D]), or the stamp's calendar month
(datetime64[M]). With numeric stamps pass a positive width w; a
case starting at t lands in the bucket labeled floor(t / w) * w,
in the stamps' own units. A calendar name on numeric stamps raises
TypeError, as does a numeric width on datetime stamps, and a width at
or below zero raises ValueError.
Each name in kpis becomes one float column, named after itself, in
the order given. Three names are derived from the log: "cycle_time"
is the mean case duration among the cell's cases, in seconds for datetime
stamps and in the stamps' own units otherwise; "n_cases" counts the
cases that start in the cell; "n_events" is the mean number of events
per case. Any other name reads a case-constant numeric or boolean column
of the log and aggregates it by its mean in the cell, so a 0/1 case flag
that switches on a period boundary comes out as an exact 0/1 column,
ready to be a did treatment; a non-numeric column raises
ValueError. The three derived names are reserved: when the log also
carries a column by a requested derived name, the request is ambiguous
and raises ValueError.
The output columns are the unit column (named after the source column),
a "period" column holding the bucket labels, then the kpi columns. A
kpi named like the unit column or "period" collides with those index
columns and raises, as does a unit column itself named "period".
With unit given the result is a :class:~ergodic.data.PanelData
keyed by (unit, "period"), the shape did, event_study, and
synthetic_control consume. With unit=None the whole log is one
series and the result is a :class:~ergodic.data.TimeSeriesData with
time="period", so interrupted_time_series consumes it directly.
Two caveats. A case is attributed to the bucket where it starts, so a case that runs long spills its effect into earlier buckets: its whole duration counts where it began, even when it finished much later. And the aggregation assumes the log is complete, with no still-open cases; a censored log drops exactly the long cases near its end, which biases late-bucket cycle times downward.
Source code in src/ergodic/process/_panel.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | |
loop(do, redo)
¶
A loop node: do runs, then any number of redo then do rounds.
Source code in src/ergodic/process/_tree.py
144 145 146 | |
net_of_tree(tree)
¶
The workflow net of a process tree, by structural recursion.
Each subtree is built between an entry and an exit place: a leaf becomes
one transition (silent for :data:~ergodic.process.TAU), seq chains
its children through fresh places, xor lets every child share the
entry and exit, par brackets per-child places with silent split and
join transitions, and loop enters through a silent step into a do
body whose exit either leaves through a silent step or runs the redo body
back to the do entry. The construction is sound for every tree (and
:func:~ergodic.process.is_sound will verify it for real).
Source code in src/ergodic/process/_net.py
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | |
par(*children)
¶
A parallel node: the children interleave. Strings become leaves.
Source code in src/ergodic/process/_tree.py
139 140 141 | |
performance(log, *, lifecycle=None)
¶
Summarize case cycle times and per-activity waiting and service.
Case durations run first event to last, in seconds, summarized as the
mean, median, and 90th percentile. Per activity, count is how many
events carry it.
With lifecycle=None the log has one timestamp per activity, which
cannot split waiting from working: mean_wait is the mean gap from the
previous event in the same case (a case's first event contributes no
wait), mean_service is None, and the result's lifecycle is False.
With lifecycle="column" the named column must exist (KeyError
otherwise). Rows valued "start" and "complete" are paired per
case and activity in time order, each start with the next complete of the
same case and activity: service is complete minus start, wait is start
minus the previous complete event in the case (a start with no earlier
complete contributes no wait), and rows that never find a partner are
counted in n_unmatched. Other lifecycle values are ignored. Durations
still run first event to last over all events.
Source code in src/ergodic/process/_performance.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
process_map(log)
¶
Build the directly-follows map of log with counts and gap timing.
Every consecutive event pair within a case traverses the arc between its
two activities; each real arc reports its count and the mean and median
gap in seconds (datetime and numeric timestamps alike). Each case also
traverses (START, first activity) and (last activity, END), whose
gaps are NaN. An activity named like a boundary sentinel raises a
ValueError, since the boundary arcs would then be ambiguous.
Source code in src/ergodic/process/_map.py
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | |
seq(*children)
¶
A sequence node: the children run left to right. Strings become leaves.
Source code in src/ergodic/process/_tree.py
129 130 131 | |
variant_table(log, *, top=None)
¶
The distinct variants, most frequent first.
Ranked by case count descending, ties by sequence. share is the
fraction of all cases on the variant, and the durations are first-to-last
event times in seconds over the variant's cases. top truncates the
table; None keeps every variant.
Source code in src/ergodic/process/_variants.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
xor(*children)
¶
An exclusive choice node: exactly one child runs. Strings become leaves.
Source code in src/ergodic/process/_tree.py
134 135 136 | |
Structural causal models¶
The Bayesian SCM: mechanisms, interventions, counterfactuals, and the identification-gated ate.
ergodic.scm.SCM
¶
A Bayesian structural causal model over a DAG or a latent-confounded ADMG.
Build with SCM(graph, data) (mechanisms auto-assigned by schema) or pass
mechanisms={node: spec} to override. spec is a built-in name
("linear", "categorical", "binary") or a
:class:~ergodic.scm.Mechanism instance. Call :meth:fit, then query.
The graph may be a Markovian DAG, or an ADMG (<-> edges) / a DAG with
latent= confounders. A bidirected edge is realized as one explicit per-unit
latent common cause: the confounded district fits jointly (the latent absorbs the
confounding) so an effect identified despite the confounding (front-door, the
general criterion with the latent modeled) computes correctly. A genuinely
non-identified effect still returns a :class:~ergodic.scm.Sensitivity through
:meth:ate, never a naked point -- identification, not the fit, decides.
Selection bias stays out of scope (identification raises on selection marks).
Source code in src/ergodic/scm/_scm.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 | |
ate(treatment, outcome, *, levels=None, engine='posterior', n=2000)
¶
The average treatment effect, routed through identification first.
identify_effect decides the return shape. Identified by back-door
adjustment (or the no-path "null" case): the SCM posterior plug-in
contrast E[Y | do(X = high)] - E[Y | do(X = low)] (a posterior
:class:~ergodic.scm.SCMResult). Not identified: a
:class:~ergodic.scm.Sensitivity (the b(c) curve over the assumed
confounding), so a naked point is impossible.
The treatment may be continuous, binary, or categorical. levels is the
(low, high) contrast; it defaults to (0.0, 1.0) (the two codes of a
binary treatment, or a unit step for a continuous one). A multi-category
treatment has no canonical pair, so it requires explicit levels (the
two category codes to contrast), mirroring the inference pillar's treated=
pattern. The outcome must be numeric (a categorical outcome has no scalar
contrast and raises); confounders and mediators may be any type -- a
categorical one is sampled in the generative model, so the back-door
g-formula marginalizes over it correctly.
Identified by back-door adjustment, the no-path "null" case, or
front-door computes the latent-aware do-contrast: a confounded district
fits its explicit latent, so the structural mechanism on the causal path fits
clean and do severs the treatment from the district latent, leaving the
identified value (the front-door mediated effect, the C1 world's 1.2). The
instrument strategy still raises with a pointer to
:func:~ergodic.inference.estimate_effect: IV identification is linear and
rests on the instrument's exclusion restriction, which the latent-district fit
does not impose, so its treatment->outcome coefficient stays on the same
non-identified ridge as the bow. The general ID functional likewise
raises for now (verify-then-widen: its latent-fit do-contrast was not held to
estimate_effect to tolerance in this phase). engine="dml" is the
cross-fit reuse path (post-MVP; raises with a pointer for now).
Source code in src/ergodic/scm/_scm.py
643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 | |
counterfactual(units, do, target, *, use_draws=True)
¶
The twin-network counterfactual for the given units under do.
Abduct each unit's structural noise on the factual graph, then predict
target on the action branch reusing it. units is a dataset (or a
mapping of node name to a column of factual values) holding every node's
factual value per unit. For invertible mechanisms the counterfactual is
exact (use_draws=False collapses to the deterministic point; the
default carries parameter uncertainty). A categorical target returns a
:class:~ergodic.scm.CategoricalResult (the answer is a distribution).
Source code in src/ergodic/scm/_scm.py
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 | |
do(intervention)
¶
A lightweight intervened view of the SCM for chaining.
scm.do({"X": 1.0}).interventional("Y") reads the post-intervention
distribution. The view shares the fit; it does not refit. A continuous
do-target takes a float; a categorical/binary do-target takes its integer
category code.
Source code in src/ergodic/scm/_scm.py
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 | |
fit(*, parallel=False)
¶
Fit every node's mechanism, per node for singletons, jointly per district.
A Markovian singleton district fits per node (cheap, exact, the MVP path); a
confounded district (nodes sharing a <->) does not factorize -- the latent
couples it -- so it fits as one joint NUTS block with the explicit latents,
the gauge, a non-centered parameterization, and a high target_accept (the
Track-D recipe). The composed posterior merges the two the same way it merges
per-node fits.
Sequential by default (robust, CI-safe). parallel=True spreads the
singleton nodes over a process pool with the BLAS/OMP threads pinned in the
workers; a confounded district's joint fit stays in the main process (its
chains already parallelize and its model is one block). Returns self.
Source code in src/ergodic/scm/_scm.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
interventional(target, do=None, *, n=2000)
¶
E[Y | do(X = x)] and its posterior, through pm.do.
do maps each intervened node to its value (broadcast to a length-n
vector internally, the verified shape gotcha; an integer code for a
categorical/binary do-target, a float for a continuous one). target must
not be an intervened node. A categorical target returns a
:class:CategoricalResult (the post-intervention per-category marginal); a
continuous target an :class:SCMResult. Carries parameter-posterior
uncertainty via the merged idata.
Source code in src/ergodic/scm/_scm.py
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 | |
ite(units, treatment, outcome, *, levels=None, shift=None)
¶
Per-unit individual treatment effects, through the twin network.
For each unit, the exact counterfactual contrast of outcome under two
treatment worlds that share that unit's abducted noise (the same-person,
two-worlds twin network). Returns one effect per unit, ordered as units --
the structural-model sibling of a CATE estimator's
:meth:~ergodic.inference.OutcomeRegression.cate, which returns a conditional
average over the noise rather than the individual's own counterfactual.
Two framings of the contrast, and you pick one:
- an absolute level contrast (the default):
Y | do(X=high) - Y | do(X=low), both worlds pinning the treatment to a fixed level for every unit, the very contrast :meth:ateaverages. Passlevels=(low, high); it defaults to(0.0, 1.0)(a unit step, or the two codes of a binary treatment). Works for a continuous, binary, or (with explicit codes) categorical treatment, and for a continuous or binary outcome -- a binary outcome's per-unit effect is its counterfactual flip, in{-1, 0, 1}. - a shift (a "shock"):
Y | do(X = x_i + shift) - Y_factual, perturbing each unit's treatment from its own factual value, against the world where nothing was done. Passshift=delta(a continuous treatment and outcome).
The two coincide for a linear-additive mechanism: the shift contrast is the
constant coef * delta for every unit, equal to the matching level step. They
part only once a mechanism is nonlinear, where the shift is the unit's local
response at its own baseline. So with the built-in linear-Gaussian mechanisms
the continuous ITE is constant across units (the coefficient), and genuine
per-unit spread comes from a binary or categorical outcome, whose logistic
response varies with each unit's baseline.
Each effect is the exact point (invertible mechanisms). For an individual's
posterior interval, call :meth:counterfactual on that one unit per arm with
use_draws=True and difference the draws.
Source code in src/ergodic/scm/_scm.py
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 | |
marginal(target, n=1000)
¶
Observational posterior-predictive draws of one node.
A continuous node returns an :class:SCMResult; a categorical/binary node a
:class:CategoricalResult (the per-category marginal), exactly as
:meth:counterfactual does for a categorical target.
Source code in src/ergodic/scm/_scm.py
322 323 324 325 326 327 328 329 330 331 | |
sample(n=1000)
¶
Observational draws of every node (read the target with :meth:marginal).
Returns the posterior predictive of the generative model over n units;
the result carries the draws of the last node in topological order (a sink)
as a summary. A categorical sink returns a :class:CategoricalResult (the
per-category marginal). Use :meth:marginal to read a specific node, or
:meth:interventional / :meth:counterfactual for a query on one.
Source code in src/ergodic/scm/_scm.py
309 310 311 312 313 314 315 316 317 318 319 320 | |
ergodic.scm.SCMResult
dataclass
¶
A posterior over a query's answer.
query names what was asked ("sample", "interventional",
"counterfactual", "ate"). target names the node read.
_draws holds the posterior draws (flattened); the accessors read them.
identified records whether the answer is a function of the data alone.
Source code in src/ergodic/scm/_result.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | |
std_error
property
¶
The posterior standard deviation of the answer.
value
property
¶
The mean of the draws (the point summary).
draws()
¶
The posterior draws of the answer.
Source code in src/ergodic/scm/_result.py
69 70 71 | |
interval(level=0.95)
¶
A credible interval at level, from the draw quantiles.
Source code in src/ergodic/scm/_result.py
73 74 75 76 77 78 79 80 81 | |
mean()
¶
The posterior mean of the answer.
Source code in src/ergodic/scm/_result.py
58 59 60 | |
ergodic.scm.CategoricalResult
dataclass
¶
A posterior over a categorical query's answer.
A categorical node's interventional or counterfactual answer is a
distribution over its categories, not a single number, so this carries the
per-category probabilities (probabilities, summing to one over
categories) and the per-unit draws when a counterfactual was asked of
specific units. identified carries the same honesty flag as
:class:SCMResult.
Source code in src/ergodic/scm/_result.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | |
mode()
¶
The most probable category.
Source code in src/ergodic/scm/_result.py
113 114 115 | |
per_unit()
¶
Per-unit counterfactual category codes, when units were queried.
Source code in src/ergodic/scm/_result.py
117 118 119 120 121 | |
ergodic.scm.Sensitivity
dataclass
¶
The effect as a function of the assumed confounding strength.
Returned in place of a point when an effect is not identified. confounding
is the grid of assumed confounding-channel values c (a covariance on the
treatment-outcome pair), effect the implied causal effect b(c) at each.
identified_at_zero is b(0), the answer under the no-confounding
assumption (the observational slope), which is where the curve meets the
identified region. obstruction is the human-readable reason (the hedge).
There is no scalar accessor on purpose: a non-identified effect is a range, not
a number. Read :meth:range, :meth:at, or the arrays.
Source code in src/ergodic/scm/_bounds.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
identified
property
¶
Always False: a :class:Sensitivity exists because the effect is not.
at(c)
¶
The implied causal effect if the confounding channel strength is c.
Linear interpolation on the swept grid; c outside the grid extrapolates
along the line, which is exact for the linear-Gaussian case.
Source code in src/ergodic/scm/_bounds.py
77 78 79 80 81 82 83 | |
range()
¶
The (min, max) implied effect over the swept confounding grid.
Source code in src/ergodic/scm/_bounds.py
71 72 73 74 75 | |
ergodic.scm.GaussianAdditive
dataclass
¶
Additive-noise Gaussian mechanism, X = b0 + w . PA + U, U ~ N(0, sigma).
The default continuous mechanism: a Bayesian-linear mean over the parents with
a half-normal prior on the noise scale. Invertible, so its counterfactual is
the exact residual U = X - (b0 + w . PA) reused under the action. A
:class:~ergodic.inference.Learner-as-mean override (carrying no parameter
posterior, abducting via the residual against the fitted mean) is planned
post-MVP; the MVP fits the Bayesian-linear mean.
In a confounded district the node may also carry incident latents: one
per-unit L ~ Normal(0, 1) per bidirected edge the node touches, entering
the mean as + loading_L * L. latents lists those latent RV names (in a
deterministic order); gauged_latents is the subset whose loading is pinned
positive (a HalfNormal) to kill the L -> -L reflection. A Markovian node
leaves both empty and reduces to the MVP exactly.
Source code in src/ergodic/scm/_mechanism.py
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 | |
abduct(fit, value, parent_values)
¶
The exact residual U = X - (b0 + w . PA) at the posterior mean.
Source code in src/ergodic/scm/_mechanism.py
440 441 442 443 | |
build(x, y)
¶
The per-node PyMC model: y ~ Normal(b0 + x_encoded . w, sigma).
x is the raw parent matrix (codes for discrete parents); it is encoded
to the one-hot design here, the same encoding the generative model uses.
Source code in src/ergodic/scm/_mechanism.py
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | |
generative_rv(nodes, n)
¶
Add this node's RV to the active generative model: Normal(b0 + PA . w, sigma).
nodes maps each (already-built) parent name to its RV. Parents are
encoded by the shared pytensor encoder, so the design matches the fit view
column for column; the RV names match the merged posterior exactly, so
sample_posterior_predictive reads the fitted draws. An incident latent
L (a per-unit Normal(0, 1) the compose helper builds first, found in
nodes) adds loading_L * L to the mean, so pm.do on this node
severs it from L while a district sibling still reads L -- the
confounding cancels in an interventional contrast.
Source code in src/ergodic/scm/_mechanism.py
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 | |
loading_name(latent)
¶
The parameter name for this node's loading on latent L (district fit).
Source code in src/ergodic/scm/_mechanism.py
294 295 296 | |
mean_per_draw(fit, parent_values)
¶
The mean per parameter-posterior draw.
parent_values is the (S, n, p) raw per-draw parent matrix (a
non-intervened parent carries its own counterfactual posterior; codes for a
discrete parent). It is one-hot encoded per draw, so the encoded design
matches the (S, P) coefficient draws. Returns (S, n). With no parents
the result is the broadcast intercept draws.
Source code in src/ergodic/scm/_mechanism.py
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 | |
structural_mean(fit, parent_values)
¶
The mean b0 + w . encode(PA) at the posterior mean.
parent_values is the (n, p) raw parent matrix (codes for discrete
parents); it is one-hot encoded internally, so the coefficients line up with
the fit. (n, p) in, (n,) out.
Source code in src/ergodic/scm/_mechanism.py
394 395 396 397 398 399 400 401 402 403 404 405 406 407 | |
ergodic.scm.BinaryGumbelMax
dataclass
¶
Bases: CategoricalGumbelMax
A binary node as a two-category Gumbel-max (the logistic mechanism).
Specializes :class:CategoricalGumbelMax to two categories; the marginal is
the Bernoulli logistic, and the Gumbel-max counterfactual is the binary
common-random-numbers flip.
Source code in src/ergodic/scm/_mechanism.py
650 651 652 653 654 655 656 657 658 659 | |
ergodic.scm.CategoricalGumbelMax
dataclass
¶
Categorical mechanism as Gumbel-max over softmax logits.
The logits are linear in the parents with a per-class slope and intercept,
fitted by the equivalent :class:Categorical likelihood (the Gumbel-max
marginal). The forward mechanism is category = argmax(logits + g) with
g an iid Gumbel vector, and the counterfactual reuses the abducted g
under shifted logits, so it is invertible. Identified up to the per-row logit
gauge, which the argmax washes out.
Source code in src/ergodic/scm/_mechanism.py
449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 | |
abduct(fit, observed_codes, parent_values, *, seed)
¶
Abduct the per-unit Gumbel noise consistent with the observed category.
Source code in src/ergodic/scm/_mechanism.py
639 640 641 642 643 644 | |
generative_rv(nodes, n)
¶
Add this node's categorical RV to the active generative model.
logits = bias + encode(PA) . weight (same per-class linear form and same
one-hot parent encoding the fit uses), then pm.Categorical samples the
node forward (the Gumbel-max marginal). The RV names match the merged
posterior, so sample_posterior_predictive reads the fitted logits. The
node's resolved class count must be passed in (it lives on the fit, not the
spec). An incident latent L adds a per-class logit term
L * loading_L (loading_L shape (K,)), so the confounding shifts
the relative class logits, and pm.do severs the node from L.
Source code in src/ergodic/scm/_mechanism.py
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 | |
loading_name(latent)
¶
The per-class logit loading parameter for this node on latent L.
Source code in src/ergodic/scm/_mechanism.py
482 483 484 | |
logits(fit, parent_values)
¶
The (n, K) class logits at the posterior mean.
parent_values is the (n, p) raw parent matrix (codes for discrete
parents); it is one-hot encoded internally so the weight rows line up.
Source code in src/ergodic/scm/_mechanism.py
593 594 595 596 597 598 599 600 601 602 603 604 605 606 | |
logits_per_draw(fit, parent_values)
¶
The (S, n, K) class logits per parameter-posterior draw.
The per-draw twin of :meth:logits: bias is (S, K) and weight
(S, P, K), so each draw's logits read bias[d] + encode(PA)[d] . w[d].
parent_values is the (S, n, p) raw per-draw parent matrix (an
intervened/categorical parent's singleton draw axis broadcasts up to S,
and is one-hot encoded per draw so the design matches the (S, P, K) weight
draws). With no parents the result is the broadcast bias draws. Used by the
counterfactual to spread a categorical ancestor's decision across draws, so
its logit-parameter posterior enters the counterfactual interval (the
posterior-mean collapse it replaces understated the width).
Source code in src/ergodic/scm/_mechanism.py
608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 | |
ergodic.scm.Mechanism
¶
Bases: Protocol
The per-node structural equation.
Implementations carry the node name and its parent names and expose the four
jobs below. The SCM holds one mechanism per node and a :class:FitResult per
mechanism after fitting. name and parents are read-only: mechanisms are
frozen value objects, so filling a blank node name returns a new instance
(:func:resolve_mechanism) rather than mutating in place.
Source code in src/ergodic/scm/_mechanism.py
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
capability
property
¶
INVERTIBLE if counterfactuals are exact, else PROBABILISTIC.
gauged_latents
property
¶
The subset of latents whose loading is pinned positive for the sign gauge.
latents
property
¶
Incident latent RV names from confounded-district loadings; () for a Markovian node.
name
property
¶
The node this mechanism is the structural equation for.
parents
property
¶
The node's observed parents, in the mechanism's design order.
fit(data, *, draws, tune, chains, seed)
¶
Fit the node on its observed parents; return the posterior.
Source code in src/ergodic/scm/_mechanism.py
133 134 135 | |
generative_rv(nodes, n)
¶
Add this node's RV to the active generative PyMC model, return the RV.
nodes maps each already-built parent name to its RV; the structural
equation reads the parents from it. Names every parameter RV with the same
scheme :meth:fit used, so the merged posterior matches by name. Called
inside a pm.Model() context by the compose helper.
Source code in src/ergodic/scm/_mechanism.py
137 138 139 140 141 142 143 144 145 | |