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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

permscore-0.1.0.tar.gz (30.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

permscore-0.1.0-py3-none-any.whl (26.6 kB view details)

Uploaded Python 3

File details

Details for the file permscore-0.1.0.tar.gz.

File metadata

  • Download URL: permscore-0.1.0.tar.gz
  • Upload date:
  • Size: 30.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.2

File hashes

Hashes for permscore-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d57555b2091a06799bf7354f5ad642a2861c281315a315a68ba57288f8d2838a
MD5 5fa20985bc8b53e7a135300c0fdc361a
BLAKE2b-256 411e3de8bf893006da53630f397f937aa7a3357400783badda9f57db04816b49

See more details on using hashes here.

File details

Details for the file permscore-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: permscore-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 26.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.2

File hashes

Hashes for permscore-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b428d8e8e4d2ebce651ac086aa271fc6fc7b68fd3506b81ed5d1c735b4c21cd3
MD5 fcbccfca3b2e06b00565c19ae3168cfb
BLAKE2b-256 ee1133bf64cdbcb73576f9d2fc313852eb931a8f765b4edb7431dba084275298

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page