Skip to main content

PermScore

Joint causal discovery when you don't know what was perturbed.

Given data from several environments — one observational, the rest interventional, with the perturbation labels not recorded — PermScore recovers both the causal DAG and which environment perturbed which node, in a single search.

Deterministic, CPU-only, no dataset-specific tuning. Dependencies are NumPy and SciPy.

pip install permscore

Quickstart

from permscore import PermScore, simulate, metrics

envs, truth = simulate.make_instance(d=10, n_env=5, seed=0)
#   envs[0]  -> observational sample, shape (n, d)
#   envs[1:] -> interventional samples; which node each one perturbed is NOT given

result = PermScore(seed=0).fit(envs).result_

result.graph            # (d, d) adjacency, acyclic by construction; graph[i, j]==1 means i -> j
result.targets          # {environment -> node or None}: one target per environment
result.target_sets      # {environment -> {nodes}}: the unconstrained flagging
result.order            # the topological order the search settled on

metrics.shd(result.graph, truth["graph"])                    # 0
metrics.exact_top1(result.targets, truth["targets"])         # accuracy 1.0

Your own data is just a list of arrays over the same variables. Sample counts may differ between environments:

envs = [X_control, X_env1, X_env2, X_env3]     # each (n_i, d), same d
result = permscore.fit(envs)

How it works

The search runs over topological orders. An order determines, for each node, which nodes may be its parents; the graph is read off by selecting parents within that order, so acyclicity holds by construction — there is no acyclicity penalty and no projection step.

For each node, parent selection and target inference come from the same local score. That score compares each environment's own fit for the node against the fit it gets from the mechanism shared with the other environments; environments that gain enough by having their own mechanism are flagged as intervened, and parents are then selected on the pooled data of the rest. The score is closed-form and decomposable, computed from pooled second-moment statistics, so each evaluation is O(1) in the number of samples — cost grows with the number of variables and environments, not with how many rows you have.

Targets are resolved by one global rectangular assignment (Hungarian), so each environment gets at most one target and the output is directly comparable with single-target baselines.

Measuring target recovery correctly

This is the part most easily got wrong, so the package makes it hard to get wrong.

A method that flags many candidate nodes per environment can look perfect under a containment metric ("is the true gene somewhere in the flagged set?") while identifying nothing. Suppose three environments and a method that flags 20 nodes for each:

from permscore import metrics

truth = {1: 0, 2: 1, 3: 2}
flag_everything = {e: set(range(20)) for e in (1, 2, 3)}

metrics.containment(flag_everything, truth)["rate"]      # 1.0   <- looks perfect
metrics.pair_scores(flag_everything, truth)["precision"] # 0.05  <- 1 of every 20 is right
metrics.exact_top1(flag_everything, truth)["accuracy"]   # 0.0   <- identified nothing

containment always reports mean_set_size alongside the rate, because the rate cannot be read without it. union_node_precision is provided for comparison with published tables that use it, with the caveat that it computes precision over the union of flagged nodes and so cannot distinguish "right node, right environment" from "flagged a node that is some other environment's target".

Default to exact_top1 and pair_scores. Report containment only next to the set size.

API

PermScore(...) estimator; .fit(envs) then .result_, .graph_, .targets_
fit(envs, **kwargs) shorthand returning PermScoreResult directly
PermScoreResult .graph, .targets, .target_sets, .order, .score, .parents(j), .edges(), .target_array()
metrics shd, skeleton_f1, exact_top1, pair_scores, containment, union_node_precision
simulate make_instance(d, n_env, seed, shift, ...)

Main parameters:

parameter default meaning
restarts 3 random permutation restarts; best-scoring order kept
margin 1.0 multiplier on the BIC penalty in target flagging; larger is more conservative
assignment True resolve targets by global matching (one per environment) rather than independent flags
abstain True allow an environment to receive no target when its best candidate is not significant
strategy "first" "first" accepts the first improving move; "best" scans all moves each round
seed 0 seeds the restarts; the method is otherwise deterministic

Scope and limitations

Stated plainly, because they determine whether this is the right tool.

  • Model class. Linear-Gaussian structural equations with additive-shift interventions. On additive-nonlinear mechanisms the linear score degrades badly. Not a drop-in for nonlinear causal discovery.
  • One target per environment is the modelling assumption behind assignment=True. Use assignment=False if environments may perturb several nodes.
  • The assignment constraint is not free. On weakly-intervened synthetic data it can cost accuracy relative to independent flagging, because one mis-scored environment can displace a correct assignment elsewhere. It is the default for output comparability, not because it is uniformly better.
  • Scaling in d is the weak point. Cost is O(1) in samples but grows with variables and environments. It is fast at tens of variables and slow at a hundred; if you have hundreds of variables, benchmark before committing.
  • Interventions must move something observable about their own target. If a perturbation leaves no trace in the measured variable it targets, no method in this family can recover it — and neither can this one.
  • Not hyperparameter-free. It requires no dataset-specific tuning: every constant is fixed across every experiment we ran. That is a different and weaker claim.

Reproducibility

The method is deterministic given seed: repeated fits are bit-identical, and the test suite asserts that rather than assuming it. Outputs on a set of fixed instances are pinned in tests/golden_values.json, and those instances were selected by mutation testing — an earlier golden set chosen for readability survived every perturbation of the scorer's constants, i.e. pinned nothing. If you change the algorithm deliberately, regenerate the pinned values and say so in the changelog; never regenerate them to make a red test go green.

The test suite ships in the source distribution:

pip download --no-binary :all: --no-deps permscore
tar xf permscore-*.tar.gz && cd permscore-*
pip install ".[test]" && pytest

License

MIT. See LICENSE.

Metadata

Release files for permscore 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for permscore 0.1.0
File Size Uploaded
permscore-0.1.0.tar.gz 30.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for permscore 0.1.0
File Interpreter ABI Platform
permscore-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 56.8 kB

Release files / permscore-0.1.0.tar.gz

Download URL permscore-0.1.0.tar.gz
Size 30.1 kB
Tags Source
SHA-256 checksum
How to use checksums
d57555b2091a06799bf7354f5ad642a2861c281315a315a68ba57288f8d2838a
BLAKE2b-256 checksum
How to use checksums
411e3de8bf893006da53630f397f937aa7a3357400783badda9f57db04816b49
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.2

Release files / permscore-0.1.0-py3-none-any.whl

Download URL permscore-0.1.0-py3-none-any.whl
Size 26.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b428d8e8e4d2ebce651ac086aa271fc6fc7b68fd3506b81ed5d1c735b4c21cd3
BLAKE2b-256 checksum
How to use checksums
ee1133bf64cdbcb73576f9d2fc313852eb931a8f765b4edb7431dba084275298
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.2

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page