Skip to content

Add XGES (Extremely Greedy Equivalence Search) - #274

Open
smellslikeml wants to merge 1 commit into
py-why:mainfrom
smellslikeml:feat/xges
Open

smellslikeml wants to merge 1 commit into
py-why:mainfrom
smellslikeml:feat/xges

Conversation

@smellslikeml

Copy link
Copy Markdown

Add XGES (Extremely Greedy Equivalence Search)

Adds XGES to causallearn/search/ScoreBased/ — a score-based causal-discovery algorithm (Extremely Greedy Equivalence Search; Nazaret & Blei, UAI 2024, arXiv:2502.19551). It learns a CPDAG by greedily applying the single best-scoring edit across insert / reverse / delete operators with incremental candidate maintenance, plus an extended-search local-optima escape (extended_search selects XGES-0 vs full XGES) — a distinct, faster engine than GES's phase-separated passes. Requested in #257.

Provenance / license

Clean-room from the paper. The reference implementation (ANazaret/XGES) is unlicensed (all-rights-reserved), so no reference code was read or vendored — the algorithm is implemented from arXiv:2502.19551 only. The xges PyPI package is used solely as an output parity oracle in the tests (its outputs are compared; its code never enters the diff). This mirrors house practice — causal-learn's own ges is a from-algorithm implementation.

Design

  • New causallearn/search/ScoreBased/XGES.py: the three-queue greedy engine (insert / reverse / delete, with the Reverse operator GES lacks and its semi-directed-path validity check) + the extended-search escape, returning a Record whose G is a GeneralGraph CPDAG — matching the ges(...) contract.
  • Reuses causal-learn's BIC LocalScoreClass, pdag2dag / dag2cpdag, and GeneralGraph. No new dependencies (pure NumPy/SciPy).
  • Score calibration: the paper's BIC penalty alpha (default 2) maps to causal-learn's lambda_value as lambda_value = alpha / 2 (paper α=2 → λ=1.0), documented so the SHD test and the xges parity comparison are meaningful.

Tests (tests/TestXGES.py)

  • SHD correctness floor — mirrors TestGES::test_ges_simulate_linear_gaussian_with_local_score_BIC: recovers a known 5-node CPDAG at SHD <= 1 (both XGES-0 and extended).
  • XGES ≡ GES — SHD(GES, XGES) == 0 on the same data (same Markov equivalence class as causal-learn's own GES — validates against a trusted in-repo reference).
  • Operator change-scores vs brute-force — insert / delete / reverse deltas match a from-scratch recompute.
  • xges PyPI parity oracle — guarded (skips if xges isn't installed); compares the learned CPDAG against the reference solver on the same data with the α↔λ-mapped penalty.

All four pass locally: the SHD floor / equivalence / operator tests need only causal-learn, and the parity oracle was run against xges 0.1.6 on PyPI — SHD(xges, causal-learn XGES) = 0 on the test graph. SHD <= 1 on a 5-node graph is a correctness floor; benchmark-scale parity across larger/denser graphs is a reasonable follow-up, and the (skip-guarded) oracle test is the harness for it.

Scope

4 files: XGES.py, tests/TestXGES.py, docs/.../XGES.rst, and the index.rst entry (mirroring the DGES layout). No changes to GES/DGES or shared machinery.


🤖 Drafted with Claude Code, human-reviewed. Implemented clean-room from the paper.

Adds `XGES` to causallearn/search/ScoreBased/ — a score-based causal-discovery
algorithm (Extremely Greedy Equivalence Search; Nazaret & Blei, UAI 2024,
arXiv:2502.19551). It learns a CPDAG by greedily applying the single
best-scoring edit across insert / reverse / delete operators with incremental
candidate maintenance and an extended-search local-optima escape, returning a
Record whose `G` is a GeneralGraph CPDAG (matching the `ges(...)` contract).

Reuses causal-learn's own BIC LocalScoreClass, pdag2dag/dag2cpdag, and
GeneralGraph. Clean-room from the paper only: the reference implementation
(ANazaret/XGES) is unlicensed, so no reference code was read or vendored; the
`xges` PyPI package is used solely as an output parity oracle in the tests.

Requested in py-why#257. Adds tests/TestXGES.py (SHD correctness floor mirroring
TestGES, XGES-equivalence, operator-vs-brute-force, and a guarded xges parity
oracle) and docs (XGES.rst + index entry).

Co-authored-by: remyx-ai[bot] <289541483+remyx-ai[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: smellslikeml <smellslikeml@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant