Skip to content

Commit 5f2a114

Browse files
PauBadiaMclaude
andcommitted
Add bundled Claude Code Agent Skill and installer
Ships a decoupler Agent Skill (SKILL.md router plus references/) so coding agents use the library correctly, along with a `decoupler-install-skills` console script to install it into ~/.claude/skills/decoupler/. Bumps version to 2.2.0 and documents the feature in the README and changelog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ec0e0ee commit 5f2a114

17 files changed

Lines changed: 1246 additions & 1 deletion

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning][].
88
[keep a changelog]: https://keepachangelog.com/en/1.0.0/
99
[semantic versioning]: https://semver.org/spec/v2.0.0.html
1010

11+
## 2.2.0
12+
13+
### Added
14+
- Bundled a Claude Code [Agent Skill](https://docs.claude.com/en/docs/claude-code/skills) that teaches coding agents to use decoupler correctly (method selection, calling conventions, output locations, priors, datasets, and benchmarking)
15+
- Added the `decoupler-install-skills` console script to install the skill into `~/.claude/skills/decoupler/` (use `--force` to refresh after upgrading, or `--print-path` to point `CLAUDE_SKILLS_PATH` at the bundled copy in place)
16+
1117
## 2.1.6
1218

1319
### Bugfixes

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ mamba create -n=dcp conda-forge::decoupler-py
7979
pip install git+https://github.com/scverse/decoupler.git@main
8080
```
8181

82+
## Claude Code Skill
83+
84+
decoupler ships an [Agent Skill](https://docs.claude.com/en/docs/claude-code/skills) for [Claude Code](https://claude.com/claude-code) that teaches the agent to use the library correctly. Install it once into your personal skills directory:
85+
86+
```bash
87+
decoupler-install-skills
88+
```
89+
90+
This copies the skill to `~/.claude/skills/decoupler/`, making it available in **every** project (re-run with `--force` after upgrading). Once installed, just ask Claude Code to do decoupler tasks ("find the most active TFs in this cell cluster", "score hallmark gene sets with ora", "build a consensus across methods") and the skill is consulted automatically. To avoid copying into your home directory, point Claude Code at the bundled copy instead:
91+
92+
```bash
93+
export CLAUDE_SKILLS_PATH="$(decoupler-install-skills --print-path)"
94+
```
95+
8296
## Release notes
8397

8498
See the [changelog][].

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ requires = [ "hatchling" ]
44

55
[project]
66
name = "decoupler"
7-
version = "2.1.6"
7+
version = "2.2.0"
88
description = "Python package to perform enrichment analysis from omics data."
99
readme = "README.md"
1010
license = { file = "LICENSE" }
@@ -38,6 +38,7 @@ dependencies = [
3838
urls.Documentation = "https://decoupler.readthedocs.io/"
3939
urls.Homepage = "https://github.com/scverse/decoupler"
4040
urls.Source = "https://github.com/scverse/decoupler"
41+
scripts.decoupler-install-skills = "decoupler._skills.install:main"
4142

4243
[dependency-groups]
4344
dev = [

src/decoupler/_skills/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Bundled Claude Code Agent Skill and its installer.
2+
3+
This subpackage keeps the decoupler Agent Skill (the ``data/`` directory) and the
4+
command that installs it (``install.py``) together and out of the main package
5+
namespace. See ``install.py`` for usage.
6+
"""
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
name: decoupler
3+
description: Use for any task involving the decoupler library — inferring biological activity/enrichment scores from omics data (bulk, single-cell, spatial). Triggers on estimating transcription factor (TF) activity, pathway activity, or gene-set enrichment from an AnnData/DataFrame; running ulm, mlm, ora, gsea, gsva, aucell, viper, zscore, waggr, mdt, udt; multi-method consensus; fetching prior knowledge (CollecTRI, DoRothEA, PROGENy, MSigDB hallmarks, OmniPath); pseudobulk and expression filtering; using bundled example datasets (pbmc3k, covid5k, erygast1k, hsctgfb, msvisium, knocktf, toy) and whether they are raw counts or normalized; ranking/plotting activity scores; or benchmarking methods against ground truth. This is a router skill — read the relevant file under references/ before writing decoupler code, because outputs land in different places and several defaults are non-obvious.
4+
---
5+
6+
# decoupler
7+
8+
`decoupler` estimates biological activities (transcription factors, pathways, gene
9+
sets) from omics data. You give it a **data matrix** (observations × features, e.g.
10+
cells × genes) and a **prior-knowledge network** (which features belong to which
11+
biological program), and it returns an **enrichment score per observation per
12+
program** — all methods sharing one unified interface. Part of scverse; works
13+
directly on `AnnData`, `pandas.DataFrame`, or raw matrices.
14+
15+
This skill is a **router**. Each topic below has a detailed reference file with exact
16+
signatures and footguns. **Read the relevant reference before writing code** — do not
17+
rely on memory of the API, because results are written to different places depending
18+
on the input type and several methods have non-obvious defaults.
19+
20+
## Two cross-cutting concepts (read these first if unsure)
21+
22+
- **[The unified method call](references/calling-convention.md)** — every method is
23+
called the same way: `dc.mt.<method>(data, net, tmin=5, ...)`. Covers the accepted
24+
input types (AnnData / DataFrame / `[matrix, obs, var]` **list**), the shared
25+
arguments (`tmin`, `raw`, `layer`, `empty`, `bsize`), and the big footguns: input
26+
must be **normalized (e.g. log1p), not raw counts**, and `net` must be a
27+
**long-format** `source`/`target`/`weight` table.
28+
29+
- **[Where the results go](references/io-and-outputs.md)** — the return type depends
30+
on the input type. **AnnData → written in place** into `.obsm["score_<method>"]`
31+
(and `.obsm["padj_<method>"]` if the method tests), returns `None`. **DataFrame /
32+
list → returns an `(es, pv)` tuple** (`pv` is `None` for non-testing methods). Read
33+
scores back out of an AnnData with `dc.pp.get_obsm`. **This is the #1 thing agents
34+
get wrong.**
35+
36+
## Task → reference file
37+
38+
| If the task is… | Read |
39+
|---|---|
40+
| **starting from scratch** — set up an end-to-end run (load data → get a net → score → rank → plot) | [references/getting-started.md](references/getting-started.md) |
41+
| how to **call a method**, what data/net formats are accepted, `tmin`/`raw`/`layer` semantics | [references/calling-convention.md](references/calling-convention.md) |
42+
| **where scores land** and how to read them back (`.obsm` vs `(es, pv)` tuple, `get_obsm`) | [references/io-and-outputs.md](references/io-and-outputs.md) |
43+
| **choosing and configuring a method** — ulm/mlm/ora/gsea/gsva/aucell/viper/zscore/waggr/mdt/udt + their kwargs | [references/methods.md](references/methods.md) |
44+
| getting **prior knowledge** — TF regulons (CollecTRI, DoRothEA), pathways (PROGENy), gene sets (MSigDB hallmarks), OmniPath, `.gmt`, organism translation | [references/priors.md](references/priors.md) |
45+
| **preprocessing** — pseudobulk from single cell, expression/sample filtering, layers, spatial kNN | [references/preprocessing.md](references/preprocessing.md) |
46+
| running **many methods at once** and combining them into a **consensus** | [references/multi-method.md](references/multi-method.md) |
47+
| **ranking** activity scores by group/ordering and **plotting** them (barplot, dotplot, volcano, network) or overlaying them on a **UMAP / spatial H&E** via scanpy | [references/ranking-and-plotting.md](references/ranking-and-plotting.md) |
48+
| **benchmarking** methods/nets against a perturbation ground truth | [references/benchmarking.md](references/benchmarking.md) |
49+
| using a **bundled example dataset** (`dc.ds.*`) — and whether its `.X` is raw counts or already normalized | [references/datasets.md](references/datasets.md) |
50+
51+
## Quick orientation (submodule map)
52+
53+
decoupler exposes everything through namespaced submodules — canonical import is
54+
`import decoupler as dc`:
55+
56+
- `dc.mt`**methods**: the enrichment/activity algorithms (`ulm`, `mlm`, `ora`,
57+
`gsea`, `gsva`, `aucell`, `viper`, `zscore`, `waggr`, `mdt`, `udt`) plus
58+
`dc.mt.decouple` (run several) and `dc.mt.consensus`. `dc.mt.show()` lists them.
59+
- `dc.op`**prior knowledge** (OmniPath): `collectri`, `dorothea`, `progeny`,
60+
`hallmark`, `resource`, `translate`.
61+
- `dc.pp`**preprocessing**: `pseudobulk`, `filter_by_expr`, `filter_by_prop`,
62+
`filter_samples`, `get_obsm`, `extract`, `swap_layer`, `knn`, `read_gmt`.
63+
- `dc.ds`**datasets**: `toy` (used in every example: `adata, net = dc.ds.toy()`),
64+
`pbmc3k`, `covid5k`, and other real examples. **They differ in normalization state**
65+
(some raw counts, some already log-normalized) — see
66+
[datasets.md](references/datasets.md) before scoring one.
67+
- `dc.tl`**tools**: `rankby_group`, `rankby_obsm`, `rankby_order`.
68+
- `dc.pl`**plotting**: `barplot`, `dotplot`, `volcano`, `network`, `obsm`, etc.
69+
- `dc.bm`**benchmarking**: `benchmark`, `metric`, `pl`.
70+
71+
## Conventions used throughout
72+
73+
- **Data** = observations × features (cells/samples × genes), **normalized** (log1p),
74+
not raw counts. **Net** = long-format DataFrame: `source` (program), `target`
75+
(feature), optional signed `weight`.
76+
- Output score matrices are observations × sources. AnnData results live in `.obsm`
77+
under `score_<method>` / `padj_<method>`.
78+
- `tmin` (default 5) silently drops sources with fewer than `tmin` targets present in
79+
the data — lower it for small/toy nets (examples use `tmin=3`).
80+
- `verbose=True` on any method prints the pruning/run log — use it to see how many
81+
sources survived `tmin`.
82+
- **Plotting**: use `dc.pl.*` for summary plots and scanpy `sc.pl.umap` / `sc.pl.spatial`
83+
(on a `dc.pp.get_obsm` score AnnData) for embedding/tissue overlays. Never reimplement
84+
a plot from matplotlib primitives — see
85+
[ranking-and-plotting.md](references/ranking-and-plotting.md).
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Benchmarking methods and nets
2+
3+
`dc.bm` evaluates how well a method + net recover a known ground truth — typically a
4+
perturbation dataset where you know which program was up/down in each sample. Use it
5+
to **choose a method** or **validate a net**, not for routine scoring.
6+
7+
## `dc.bm.benchmark`
8+
9+
```python
10+
dc.bm.benchmark(
11+
adata, # AnnData with a perturbation ground truth in .obs
12+
net, # a net, or a dict of {name: net} to compare several
13+
metrics=None, # e.g. ["auroc", "auprc"] (None = defaults)
14+
groupby=None, # obs column(s) to stratify the evaluation
15+
runby="expr", # evaluate per-observation ("expr") or per-source
16+
sfilt=False, # filter sources to those with a ground-truth label
17+
thr=0.1, emin=5,
18+
kws_decouple=None, # kwargs forwarded to dc.mt.decouple (methods, args, tmin, ...)
19+
verbose=False,
20+
)
21+
```
22+
23+
- Internally runs `dc.mt.decouple`, so pass method selection and per-method args via
24+
`kws_decouple` (e.g. `kws_decouple={"methods": ["ulm", "mlm"], "tmin": 3}`).
25+
- Pass a **dict of nets** to benchmark several priors head to head.
26+
- The ground truth (which source is perturbed, and its sign) must be encoded in
27+
`adata.obs` — see the decoupler benchmarking tutorial for the expected columns.
28+
29+
## Metrics and plots
30+
31+
- `dc.bm.metric` — the scoring functions: `auc`, `fscore`, `qrank`, `hmean`,
32+
`dict_metric`.
33+
- `dc.bm.pl` — plots of benchmark results: `auc`, `bar`, `fscore`, `qrank`, `summary`.
34+
35+
```python
36+
res = dc.bm.benchmark(adata, {"collectri": net_a, "dorothea": net_b},
37+
kws_decouple={"methods": ["ulm", "mlm"]})
38+
dc.bm.pl.summary(res) # compare methods × nets
39+
```
40+
41+
## When to use
42+
43+
- Deciding **which method** to trust for your data/organism.
44+
- Comparing **candidate nets** (e.g. CollecTRI vs DoRothEA levels).
45+
- Sanity-checking a **custom net** against a perturbation benchmark
46+
(`dc.ds.knocktf` and similar provide ground-truth datasets).
47+
48+
## Footguns
49+
50+
- Benchmarking is only meaningful with a **real ground truth** — a toy net won't tell
51+
you anything.
52+
- It re-runs scoring internally; keep `kws_decouple` consistent with how you intend to
53+
score for real, or the comparison won't transfer.
54+
55+
## Related references
56+
57+
[multi-method.md](multi-method.md) (`decouple`, which `benchmark` wraps),
58+
[methods.md](methods.md) (candidate methods),
59+
[priors.md](priors.md) (candidate nets).
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# The unified method call
2+
3+
Every enrichment method in `dc.mt` is a callable object with the **same signature**.
4+
Learn it once and it applies to `ulm`, `mlm`, `ora`, `gsea`, `gsva`, `aucell`,
5+
`viper`, `zscore`, `waggr`, `mdt`, `udt` (and `dc.mt.decouple`).
6+
7+
```python
8+
import decoupler as dc
9+
10+
adata, net = dc.ds.toy() # AnnData (obs × var) + long-format net
11+
dc.mt.ulm(adata, net, tmin=3) # writes adata.obsm["score_ulm"], ["padj_ulm"]
12+
```
13+
14+
## Shared signature
15+
16+
```python
17+
dc.mt.<method>(
18+
data, # AnnData | pandas.DataFrame | [matrix, obs_names, var_names]
19+
net, # long-format DataFrame: source, target, [weight]
20+
tmin=5, # drop sources with < tmin targets present in the data
21+
raw=False, # use adata.raw instead of adata.X
22+
layer=None, # use adata.layers[layer] instead of adata.X (passed via kwargs)
23+
empty=True, # drop all-zero observations/features before scoring
24+
bsize=250_000, # batch size (rows) for sparse / backed matrices
25+
verbose=False,
26+
**kwargs, # method-specific args — see references/methods.md
27+
)
28+
```
29+
30+
`layer=` is accepted even though it is not in the explicit signature — it is forwarded
31+
through `**kwargs` to the runner. Use `layer=` **or** `raw=`, not both.
32+
33+
## Accepted input types (`data`)
34+
35+
| Form | What it is | Returns |
36+
|---|---|---|
37+
| `AnnData` | scored on `.X` (or `.raw`/`layer`) | **`None`** — results written to `.obsm` in place |
38+
| `pandas.DataFrame` | observations (rows) × features (cols) | `(es, pv)` tuple of DataFrames |
39+
| `list` `[matrix, obs_names, var_names]` | numpy/sparse matrix + name arrays | `(es, pv)` tuple of DataFrames |
40+
41+
See [io-and-outputs.md](io-and-outputs.md) for the return details.
42+
43+
### Footgun — the matrix form must be a `list`, not a `tuple`
44+
45+
```python
46+
# WRONG — a tuple raises "mat must be a list of [matrix, samples, features]"
47+
es, pv = dc.mt.ulm((X, obs, var), net)
48+
49+
# RIGHT — wrap it in a list
50+
es, pv = dc.mt.ulm([X, obs, var], net)
51+
```
52+
53+
## The three footguns that silently give wrong results
54+
55+
1. **Input must be normalized, not raw counts.** decoupler expects
56+
log-normalized expression — the same matrix you would use for differential
57+
expression. **When normalizing, use `sc.pp.normalize_total(adata, target_sum=1e4)`**
58+
followed by `sc.pp.log1p(adata)`. Passing raw counts runs without error but produces
59+
meaningless scores. If your normalized data is in a layer, pass
60+
`layer="lognorm"`; if in `.raw`, pass `raw=True`. **Check before normalizing:** if
61+
`.X` is integer-valued with a large max it is raw counts; if it is small floats
62+
(often with `adata.uns["log1p"]`) it is already normalized — do not normalize twice.
63+
The bundled datasets differ; see [datasets.md](datasets.md) (e.g. `pbmc3k` is already
64+
log-normalized, `covid5k` is raw counts).
65+
66+
2. **`net` must be long-format** with columns `source`, `target`, and (for weighted
67+
methods) `weight` — one row per (program, feature) edge. A wide/matrix net or wrong
68+
column names will fail or misbehave. See [priors.md](priors.md).
69+
70+
3. **`tmin` silently prunes.** Sources with fewer than `tmin` (default 5) targets
71+
present in `data` are dropped *before* scoring — they simply won't appear in the
72+
output. On small or toy nets this can drop everything; the examples use `tmin=3`.
73+
Run with `verbose=True` to see `<method> - X sources with < tmin targets` and how
74+
many survived.
75+
76+
## Listing and introspecting methods
77+
78+
```python
79+
dc.mt.show() # DataFrame: name, desc, stype, weight, test, limits, reference
80+
```
81+
82+
- `weight` — whether the method uses signed edge weights (`True` for ulm/mlm/viper/…).
83+
- `test` — whether it produces p-values (`padj`). `False` for aucell/gsva/mdt/udt.
84+
- `stype` / `limits` — value type and output range (e.g. aucell `(0, 1)`).
85+
86+
## Related references
87+
88+
[io-and-outputs.md](io-and-outputs.md) (return types, reading `.obsm` back),
89+
[methods.md](methods.md) (per-method kwargs and how to choose one),
90+
[priors.md](priors.md) (building/fetching `net`),
91+
[preprocessing.md](preprocessing.md) (getting a normalized matrix / layers).

0 commit comments

Comments
 (0)