Skip to main content

seq2cause

Turn any sequence of discrete events into a causal graph using autoregressive models (LLaMA, GPT, RNN, Mamba).

DOI PyPI version License: MIT Python 3.8+

seq2cause is a Python library for causal discovery on discrete event sequences. It treats any autoregressive model as a density estimator and runs parallelized conditional-independence tests on GPU, so you can recover what caused what directly from a sequence of logs, codes, or symbols.

🚀 Key Features

  • Bring your own model: plug in any HuggingFace/PyTorch model (GPT-2, LLaMA, RNN) trained on your own event sequences.
  • Scales to long sequences: memory grows linearly with vocabulary and sequence length, well suited to sparse, high-dimensional streams like vehicle diagnostics, server logs, or user journeys.
  • Multi-GPU ready: powered by 🤗 Accelerate, tested on CPU, Apple Silicon (MPS), and NVIDIA CUDA, single or multi-device.
  • Delayed effects: causal lags are identifiable up to the sequence length.
  • Event-to-event and event-to-outcome graphs from a single sequence. Aggregating these into a global causal graph across many sequences is on the roadmap.

📦 Installation

pip install seq2cause

⚡ Quick Start

Recover a causal graph from your own sequence of events, using your own model. No known generator and no labeled ground truth needed. HFModelAdapter wraps any HuggingFace causal LM (a fine-tuned checkpoint, LlamaForCausalLM.from_pretrained(...), etc.); here we use a small, randomly initialized model just so the snippet runs standalone. This exact example is part of the test suite (tests/test_readme_example.py).

import torch
from transformers import LlamaConfig, LlamaForCausalLM
from seq2cause.adapters import HFModelAdapter
from seq2cause.diagnostics import compute_cmi_matrix
from seq2cause.threshold import AdaptiveThreshold

# 1. Your model (any HF causal LM) and your own event sequence (token ids).
vocab_size = 20
model = LlamaForCausalLM(LlamaConfig(
    vocab_size=vocab_size, hidden_size=32, intermediate_size=64,
    num_hidden_layers=2, num_attention_heads=2, max_position_embeddings=32,
)).eval()
adapter = HFModelAdapter(model, vocab_size=vocab_size)
sequence = torch.randint(0, vocab_size, (20,))  # your real event sequence

# 2. Conditional Mutual Information between every (cause, effect) pair.
cmi_matrix = compute_cmi_matrix(adapter, sequence, context_len=3, n_particles=32)

# 3. Turn CMI scores into a binary causal graph, no labels needed.
causal_graph = AdaptiveThreshold().causal_graph(cmi_matrix)

🧪 Evaluation (against a known generator)

To validate the method itself, seq2cause.scm.NonlinearSCM is a synthetic generator with a known ground-truth causal graph. This lets you measure recall and F1 directly, and pick a threshold by maximizing F1 on a held-out validation split.

import torch
from seq2cause.scm import create_scm
from seq2cause.diagnostics import ground_truth_adjacency, compare_intervention_strategies
from seq2cause.threshold import select_threshold_by_validation

torch.manual_seed(0)

# 1. A sequence from a known generator, so we can check our own answers.
scm, sequences = create_scm(vocab_size=15, memory=3, length=20, seed=0, sparsity=0.5)
sequence = sequences[0]

# 2. Ground-truth edges, only possible because the generator is known.
adjacency = ground_truth_adjacency(scm, sequence, threshold=0.05, n_counterfactuals=16)

# 3. Same recovery step as Quick Start.
results = compare_intervention_strategies(
    scm, sequence, context_len=3, adjacency=adjacency, n_particles=32, max_lag=3,
)
cmi_matrix = results["atomic"].cmi_matrix

# 4. With labels available, pick tau by maximizing F1 instead of guessing.
scores, labels = cmi_matrix.flatten(), adjacency[3:, 3:].flatten()
result = select_threshold_by_validation(scores, labels, delta=0.05)
causal_graph = cmi_matrix >= result.tau
print(result.summary())  # e.g. F1=0.800, precision=0.800, recall=0.800

📚 How It Works

seq2cause treats an autoregressive model's own next-token predictions as a density estimator. For each candidate (cause, effect) pair, it compares the model's predicted probability of the effect with and without the cause intervened on (seq2cause.sampling.do_interventions), and reports the Conditional Mutual Information between them (seq2cause.diagnostics).

Graph Types

  • Event-to-event (implemented here): the TRACE algorithm, using CMI to recover a causal graph from a single sequence.
  • Event-to-outcome and global aggregation: the OSCAR/CARGO algorithms from the paper aren't implemented in this repo yet.

Also on the roadmap: causal discovery for time series, using normalizing flows or AR models.

🖥️ GPU / Multi-GPU Acceleration

seq2cause.core.SampleLevelCausalDiscovery wraps your model and dataloader with 🤗 Accelerate, so the same code runs unchanged on CPU, one GPU, or many (accelerate launch --multi_gpu). Tested on CPU, Apple Silicon (MPS), and NVIDIA CUDA.

A tqdm progress bar shows batch/context progress along with a rough estimate of the VRAM or RAM the current step needs (a lower bound, see seq2cause.utils.estimate_tensor_bytes), so you get an early signal before a run runs out of memory.

To test multi-GPU correctness without real GPU hardware, accelerate launch --num_processes=2 --cpu your_script.py runs two real processes over the gloo backend, exercising the same sharding and gather() code paths a real multi-GPU launch uses. tests/test_multi_gpu.py does exactly this: it checks that each process gets a disjoint shard of the data, that gather() reassembles it correctly, and that every process produces a valid adjacency matrix.

🎯 Threshold Selection

No labeled ground truth (the realistic case, see Quick Start above): AdaptiveThreshold is the recommended default. It fits Otsu once, globally, anchored at lag 1, then decays it exponentially toward an Otsu-fit floor as lag increases. Fitting Otsu independently per lag instead starves deeper lags of samples and costs 15 to 18 F1 points.

from seq2cause.threshold import AdaptiveThreshold

causal_graph = AdaptiveThreshold().causal_graph(cmi_matrix)

Other unsupervised options (mad_threshold, percentile_threshold, gmm_threshold) are available in seq2cause.threshold but were weaker in our own testing.

Labeled ground truth available (Evaluation above, or benchmarking): a fixed CMI threshold doesn't transfer across generators or backbones. Following an independent replication (Chadyuk, Zhang, and Kucukates, "Replicating TRACE: A Practitioner's Guide to Its Threshold and Particle Budget", LotusFlare Inc., Aug 2026), the recommended practice is to select the threshold by maximizing F1 on a held-out validation split instead of hardcoding a constant.

from seq2cause.threshold import select_threshold_by_validation

result = select_threshold_by_validation(
    cmi_scores,  # per-pair CMI on a held-out validation split
    labels,      # aligned boolean ground-truth edge labels
    delta=0.05,  # the KL margin used to define a ground-truth edge, if known
)
print(result.summary())
tau = result.tau

Alternative Intervention Constructions

An independent replication found that the paper's original "full" staircase construction (seq2cause.sampling.do_interventions) tends to collapse when the true causal strength decays with lag. do_interventions supports a few strategies to work around this. "atomic" is the recommended default, though do_interventions itself still defaults to "full" for backward compatibility with the paper's original results.

  • atomic (recommended): only the candidate-cause position is randomized, everything else stays real.
  • full (default): the original staircase construction.
  • windowed: preserves a trailing local-context window before each candidate effect.
  • independent_mediator: draws the cause and mediator noise independently instead of from a shared tensor.

Run python scripts/snr_diagnostic.py for a self-contained comparison of all four on a synthetic oracle SCM.

Sparse / Bounded-Memory Construction

If you know, or are willing to assume, an upper bound on the true causal lag (for example a NonlinearSCM(memory=m) generator), compute_cmi_matrix_sparse recovers the same causal graph as the unbounded computation for a fraction of the compute on long sequences. It slides a short local window across the sequence instead of processing it all at once.

from seq2cause.diagnostics import compute_cmi_matrix_sparse

cmi_matrix = compute_cmi_matrix_sparse(
    adapter, sequence, context_len=5, memory=3, n_particles=32,
)  # context_len must be greater than memory

This is exact, not an approximation, whenever memory truly bounds the lag. scripts/evaluate_sparse_vs_full.py confirms this empirically: pooled F1 is typically within 0.01 to 0.02 of the unbounded computation, with a 2x to 5x speedup that grows with sequence length.

python scripts/evaluate_sparse_vs_full.py --seq-len 400 --memory 3 --context 4

Citation

If you use seq2cause in your research, please cite our works:

@software{math2026seq2cause,
  author = {Math, Hugo},
  title = {seq2cause: Sample- and Population-Level Causal Discovery from Event Sequences using Autoregressive Models},
  year = {2026},
  publisher = {Zenodo},
  doi = {10.5281/zenodo.19068730},
  url = {https://doi.org/10.5281/zenodo.19068730},
  version = {0.1.7}
}
@inproceedings{
math2026your,
title={Your Autoregressive Model Already Reveals the Causal Graph},
author={Hugo Math and Rainer Lienhart},
booktitle={ICML 2026 Workshop on Structured Probabilistic Inference {\&} Generative Modeling},
year={2026},
url={https://openreview.net/forum?id=Q66hINx9fA}
}
@inproceedings{
math2025oneshot,
title={One-Shot Multi-Label Causal Discovery in High-Dimensional Event Sequences},
author={Hugo Math and Robin Sch{\"o}n and Rainer Lienhart},
booktitle={NeurIPS 2025 Workshop on CauScien: Uncovering Causality in Science},
year={2025},
url={https://openreview.net/forum?id=z7NT8vGWC2}
}
@inproceedings{
math2025towards,
title={Towards Practical Multi-label Causal Discovery in High-Dimensional Event Sequences via One-Shot Graph Aggregation},
author={Hugo Math and Rainer Lienhart},
booktitle={NeurIPS 2025 Workshop on Structured Probabilistic Inference {\&} Generative Modeling},
year={2025},
url={https://openreview.net/forum?id=1HZfpuDVeW}
}

License

This project is licensed under the MIT License, see the LICENSE file for details.

Building

Ruff keeps the code clean; pre-commit runs it automatically before every commit.

pre-commit run --all-files
git add .
git commit -m "your message"
git push origin main

Metadata

Release files for seq2cause 0.1.7

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

Source distribution (sdist)

Source distribution for seq2cause 0.1.7
File Size Uploaded
seq2cause-0.1.7.tar.gz 1.5 MB Details

Built distribution (wheel)

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

Total release size: 1.6 MB

Release files / seq2cause-0.1.7.tar.gz

Download URL seq2cause-0.1.7.tar.gz
Size 1.5 MB
Tags Source
SHA-256 checksum
How to use checksums
8846b3dba048ca63c41cfa608efeabd6143f497e6e415876b01f981a9a190bde
BLAKE2b-256 checksum
How to use checksums
eae0ad245439bfde08cb3fdcad0575e5c5ea882033dc946e6eb569b87fcc216e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 27, 2026.

Transparency log

Release files / seq2cause-0.1.7-py3-none-any.whl

Download URL seq2cause-0.1.7-py3-none-any.whl
Size 52.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
365e88c660f78d2fbb47b29733ec08d5cfbccf434a31bd0255eab58d720fbbc3
BLAKE2b-256 checksum
How to use checksums
c78eb3027c40c57259732785604a307d3eea367461491b551b0522d996e30c95
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 27, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.9

2 release files

This release

0.1.7 This release

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.0

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