Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ AREE/
| `aree check-study-scope --branch <b>` | Fail if a study branch changes files its study does not own (run by CI) |
| `aree harmonize --study <id> [--input <file>]` | Harmonize a study (or one processed table) into the evidence table |
| `aree meta-analyze [--phenotype <p>] [--feature-type <t>] [--level gene\|orthogroup] [--include-multi-copy]` | Random-effects meta-analysis over the evidence table, with BH-adjusted p-values per phenotype / feature-type family; `--level orthogroup` pools species' gene-level estimates across species through single-copy orthogroups |
| `aree build-cross-species-cards [--phenotype <p>] [--feature-type <t>] [--max-adjusted-p <q>] [--all-cards] [--include-multi-copy]` | Tier orthogroup-level (cross-species) results into `reports/evidence_cards/cross_species/candidates.tsv` and write a card per significant row showing every species side by side |
| `aree build-evidence-cards [--phenotype <p>] [--feature-type <t>] [--max-adjusted-p <q>] [--all-cards]` | Rank every candidate into `reports/evidence_cards/candidates.tsv` and write an evidence card for each one with a BH-adjusted p ≤ `q` (default 0.05, pooled or in any contributing study) |
| `aree top-candidates [--n <N>] [--candidates <path>] [--out <path>]` | Regroup an existing `candidates.tsv` by phenotype and write the top `N` (default 10) per phenotype, ranked by tier then score, to `reports/top_candidates_summary.md` |
| `aree build-crosswalk --taxid <n> [--slug <s>]` | Build a real identifier crosswalk from NCBI Gene + UniProtKB for a registered species |
Expand Down
7 changes: 5 additions & 2 deletions docs/implementation_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@ worth less than one that says what it got wrong.
- Cross-species pooling. Orthogroups exist (OrthoFinder run
`orthofinder3.1.5_2026-10-13`, attached to evidence at harmonization) and
orthogroup-level meta-analysis exists (`aree meta-analyze --level
orthogroup`), but cross-species candidates, their evidence cards and the
dashboard gate are not built; see WP6 in mussel_clam_integration_plan.md.
orthogroup`), as do cross-species candidates and their evidence cards
(`aree build-cross-species-cards`). On real data the oyster and the first
Manila clam study pool through 5,681 single-copy orthogroups, none past the
replication gate yet (one clam study). The dashboard gate is not built; see
WP6 in mussel_clam_integration_plan.md.
- Improving UniProt coverage beyond the 8.4% of genes that UniProtKB itself
cross-references to NCBI GeneID — most likely by mapping RefSeq protein
accessions through the NCBI annotation rather than relying on UniProt xrefs.
Expand Down
81 changes: 81 additions & 0 deletions docs/interpreting_candidate_scores.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,87 @@ BH-adjusted p-values with the family size, and a forest-style table of
per-study effect estimates with each study's own adjusted p, so a reviewer can
see exactly which inputs drove the score rather than trusting a single number.

## Cross-species candidates

`aree build-cross-species-cards` tiers the orthogroup-level meta-analysis
(see [interpreting_meta_analysis.md](interpreting_meta_analysis.md#cross-species-results---level-orthogroup)):
one row per orthogroup, phenotype and feature type, pooling each species' own
gene-level estimate. The rule is the same as for single-species candidates:
**hard gates first, score second.** It writes, under
`reports/evidence_cards/cross_species/`, a `candidates.tsv` with every row and
its component breakdown, an `index.json`, and a card for each row whose pooled
adjusted p, or any one species' gene-level adjusted p, is at or below
`--max-adjusted-p`.

The five convergence gates, each named in the `gates_failed` column when it
fails:

| Gate | Passes when |
|---|---|
| `replication` | at least two species each have k ≥ 2 studies of their own |
| `direction` | every species' estimate has the same sign |
| `significance` | the pooled cross-species BH-adjusted p ≤ 0.05 |
| `phenotype_relevance` | the weakest evidence class across all species' contributing records is more than exposure-only |
| `quality` | each species' own quality flags are within the gene-level limit (the weakest species decides) |

The tiers:

| Tier | Meaning |
|---|---|
| `cross_species_convergence` | every gate passes |
| `cross_species_emerging` | species agree in sign and the pooled estimate is significant, but another gate fails, usually replication |
| `cross_species_discordant` | species that are each significant at gene level disagree in sign; shown, never dropped |
| `cross_species_unsupported` | the pooled estimate is not significant, or species disagree in sign only because one of them is noise |

The direction gate is judged on point estimates, so any sign disagreement
fails it. Only a disagreement between species that are each significant at
gene level is called discordant. Two null estimates disagree in sign about
half the time: on the first real oyster-clam pool, 2,880 of 2,886 sign
disagreements involved a non-significant species, and only 6 were reversals
between significant ones.

The cross-species score uses its own weights, which sum to 100:

| Component | Weight | What it measures |
|---|---|---|
| `per_species_replication_score` | 20 | the smallest per-species study count, saturating at 2 = 0.5 |
| `n_species_score` | 15 | number of species, saturating at 2 = 0.5 |
| `direction_consistency_score` | 15 | share of species agreeing with the majority sign |
| `effect_magnitude_score` | 10 | absolute pooled cross-species effect |
| `significance_score` | 10 | pooled BH-adjusted p, on the −log10 scale |
| `phenotype_relevance_score` | 10 | weakest evidence class across all species' records |
| `orthology_score` | 8 | copy class: `single_copy_all` 1.0, `single_copy` and `demo_crosswalk` 0.9, `multi_copy` 0.3 |
| `sample_size_score` | 5 | total biological replicates across species |
| `mapping_confidence_score` | 4 | worst identifier mapping behind any species' estimate |
| `quality_score` | 3 | the weakest species' own gene-level quality (`1 - n_flags/5`) |

Between-species I² is subtracted as a penalty of up to 15 points. The
smallest per-species study count carries the most weight because a
cross-species claim is only as strong as its weakest species.

A cross-species card shows each species' gene, original identifiers, studies,
tissue, life stage, stressor, sample size, effect with 95% CI and gene-level
adjusted p side by side, then the pooled estimate and between-species
heterogeneity. It names the orthogroup run, or says the orthogroup is the demo
crosswalk's illustrative one. It also warns when the species' studies differ
in stressor, tissue or life stage, because species and study design are then
confounded. Agreement across species shows an association recurs in
orthologous genes. It does not show the genes do the same thing in each
species.

On the demo, oyster and mussel larval studies give five emerging rows and
three unsupported rows. None converges, because the demo mussel has one study
per gene. The demo's one sign disagreement involves a non-significant mussel
estimate, so it is unsupported, not discordant.

On real data (2026-10-16), the three harmonized oyster thermal-tolerance studies and TUCCI2025_HEAT_PRIMING pool through 5,681 single-copy orthogroups: 64 emerging, 6 discordant, 5,611 unsupported, none convergent. Every emerging row fails three gates:
replication (one clam study), phenotype relevance (the clam comparison is
exposure-only), and quality (each species' own flags exceed the gene-level
limit, as they do for its single-species candidates). The 6 discordant rows
are oyster genes with small significant effects against clam genes with large
significant effects of opposite sign. The two studies differ in stressor
timing, so they are leads to check, not established species differences.

## Related documentation

- [interpreting_meta_analysis.md](interpreting_meta_analysis.md)
Expand Down
12 changes: 9 additions & 3 deletions docs/interpreting_meta_analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,15 @@ How to read a row:
- `adjusted_p_value` is Benjamini-Hochberg within each phenotype, feature type
and origin family of orthogroup-level tests.

As of 2026-10-14, the only cross-species rows come from the demo: oyster and
mussel larval studies sharing demo orthogroups, none past the gate. Real
cross-species rows need real evidence from a second species on `main`.
The first real cross-species rows came on 2026-10-16, once the first real
clam study reached `main`: oyster and Manila clam thermal-tolerance evidence
pools through 5,681 single-copy orthogroups (4,890 multi-copy orthogroups set
aside). None is past the gate, because the clam has one study. The demo adds
oyster and mussel larval rows sharing demo orthogroups, also below the gate.

`aree build-cross-species-cards` tiers these rows and writes a card for each
one that shows every species side by side; see
[interpreting_candidate_scores.md](interpreting_candidate_scores.md#cross-species-candidates).

## Related documentation

Expand Down
33 changes: 27 additions & 6 deletions docs/mussel_clam_integration_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

**Status, 2026-10-13:** WP0 to WP4 are implemented. WP5 has its backlog and a
first study (TUCCI2025_HEAT_PRIMING, Manila clam, reanalyzed). WP6 has its
orthogroup run and orthogroup-level meta-analysis; cross-species candidates
and the dashboard gate are not built yet (see the entries in section 5). Everything else is a proposal. This plan describes the work needed
orthogroup run, orthogroup-level meta-analysis and cross-species candidates
with their evidence cards; the dashboard gate is not built yet (see the entries in section 5). Everything else is a proposal. This plan describes the work needed
to take AREE from a single-species (*Magallana
gigas*) evidence engine to one that holds mussel and clam evidence, first
species-by-species and then across species through orthology.
Expand Down Expand Up @@ -442,7 +442,7 @@ k ≥ 2 per taxon.

### WP6. Cross-species orthology and pooling (the new science)

**Status: orthogroups done, 2026-10-13; pooling not started.** What exists:
**Status: orthogroups done, 2026-10-13; pooling and cross-species candidates done, 2026-10-14; dashboard gate not started.** What exists:

- `aree fetch-reference --proteins` writes one protein per gene (the longest)
for each species, keyed by NCBI GeneID; all seven reference assemblies have
Expand Down Expand Up @@ -474,12 +474,33 @@ Q, I-squared and tau-squared, per-species estimates and study counts, and a
Multi-copy orthogroups are set aside by default, as are orthogroups missing
from the active run. On the demo, 8 orthogroups pool oyster with mussel
(larval viability), none past the gate; on all committed real evidence
(465,531 oyster records), nothing pools across species yet, correctly.
(465,531 oyster records), nothing pooled across species yet, correctly, until
the first clam study merged (see below).
Building it found that the demo's illustrative orthogroup IDs collide with
real OrthoFinder IDs, so simulated evidence never consults the run (tested).

Still to build: the cross-species candidate category, evidence cards that
show both species, and the dashboard gate. The
**Cross-species candidates and cards: done, 2026-10-14.** `aree
build-cross-species-cards` tiers each orthogroup row behind five named gates
(replication, direction, significance, phenotype relevance, quality) into
`cross_species_convergence`, `cross_species_emerging`,
`cross_species_discordant` or `cross_species_unsupported`, with a transparent
weighted score and a between-species I-squared penalty. Each card shows every
species' estimate side by side, the pooled estimate, the orthogroup run and
the gates failed. Single-species cards now name their orthogroup. On the
demo: 5 emerging, 3 unsupported, none convergent.

First real cross-species rows, 2026-10-16, after TUCCI2025_HEAT_PRIMING
merged: oyster and Manila clam thermal-tolerance evidence pools through 5,681
single-copy orthogroups, giving 64 emerging, 6 discordant and 5,611
unsupported rows, none convergent, with 603 cards. Real data changed two
rules. "Discordant" now requires species that are each significant at gene
level and disagree in sign: 2,880 of 2,886 sign disagreements involved a
non-significant species, which is noise, not a reversal. Quality is now
judged per species, because the union of flags across species always
exceeded the limit. PERUZZA2023_HEATWAVE (claimed, PR #64) is the second clam
study, the one that can bring the clam to k = 2.

Still to build: the dashboard gate. The
OrthoFinder run used `-M dendroblast` (OrthoFinder 3 has no orthogroups-only
mode) and only its `Orthogroups.tsv` is used; hierarchical orthogroups are a
possible refinement.
Expand Down
37 changes: 37 additions & 0 deletions src/aree/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,43 @@ def build_evidence_cards_cmd(phenotype, feature_type, max_adjusted_p, all_cards)
click.echo(f" {tier}: {tiers[tier]}")


@main.command("build-cross-species-cards")
@click.option("--phenotype", default=None, help="Phenotype ontology term id to filter on (default: all).")
@click.option("--feature-type", default=None, help="Feature type to filter on (default: all).")
@click.option("--max-adjusted-p", default=DEFAULT_MAX_ADJUSTED_P, show_default=True, type=float,
help="Render a card only when the pooled cross-species adjusted p, or any one species' "
"gene-level adjusted p, is at or below this value. Every row is still listed in "
"reports/evidence_cards/cross_species/candidates.tsv.")
@click.option("--all-cards", is_flag=True, help="Render a card for every orthogroup row.")
@click.option("--include-multi-copy", is_flag=True,
help="Also pool multi-copy orthogroups where each species has evidence for exactly one gene.")
def build_cross_species_cards_cmd(phenotype, feature_type, max_adjusted_p, all_cards, include_multi_copy):
"""Tier orthogroup-level (cross-species) results and write cross-species evidence cards."""
from reporting.cross_species_cards import TIER_ORDER as CROSS_TIERS
from reporting.cross_species_cards import build_cross_species_cards_report

try:
report = build_cross_species_cards_report(
phenotype=phenotype, feature_type=feature_type, max_adjusted_p=max_adjusted_p,
all_cards=all_cards, include_multi_copy=include_multi_copy)
except (FileNotFoundError, ValueError) as exc:
click.echo(click.style(str(exc), fg="red"))
sys.exit(1)
if report.n_candidates == 0:
click.echo("No orthogroup has gene-level estimates from two or more species, so there are no "
"cross-species candidates. Harmonize a study of a second species first.")
return
click.echo(click.style(
f"Tiered {report.n_candidates} cross-species rows ({report.candidates_path}); "
f"wrote {len(report.index)} cards to {report.candidates_path.parent}/", fg="green"))
counts = {}
for row in report.index:
counts[row["tier"]] = counts.get(row["tier"], 0) + 1
for tier in CROSS_TIERS:
if tier in counts:
click.echo(f" {tier}: {counts[tier]}")


@main.command("top-candidates")
@click.option("--n", "n", default=10, show_default=True, type=int,
help="Number of top-ranked candidates to show per phenotype.")
Expand Down
16 changes: 16 additions & 0 deletions src/meta_analysis/orthogroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,15 @@
"orthogroup_id", "phenotype", "feature_type", "simulated", "species_taxid",
"feature_id_standardized", "k_studies", "studies", "pooled_effect", "pooled_se",
"p_value", "adjusted_p_value",
# Carried from the gene-level row so cross-species prioritization can judge
# relevance, quality and context without re-running the gene-level pool.
"contributing_evidence_ids", "total_sample_size", "distinct_tissues",
"distinct_life_stages", "distinct_stressors", "mapping_confidences", "quality_flags_union",
]
# Defaults for gene-level rows built by hand (tests) without these columns.
_CARRIED_DEFAULTS = {"contributing_evidence_ids": "", "total_sample_size": 0, "distinct_tissues": 0,
"distinct_life_stages": 0, "distinct_stressors": "", "mapping_confidences": "",
"quality_flags_union": ""}


def gene_to_orthogroup(evidence: pd.DataFrame) -> dict[tuple, str]:
Expand Down Expand Up @@ -87,6 +95,13 @@ def _key(value):
return text


def _carried(row: pd.Series, column: str):
value = row.get(column)
if value is None or (isinstance(value, float) and value != value):
return _CARRIED_DEFAULTS[column]
return value


def run_orthogroup_meta_analysis(
gene_results: pd.DataFrame,
evidence: pd.DataFrame,
Expand Down Expand Up @@ -166,6 +181,7 @@ def run_orthogroup_meta_analysis(
"k_studies": int(r["k_studies"]), "studies": r["studies"],
"pooled_effect": r["pooled_effect"], "pooled_se": r["pooled_se"],
"p_value": r["p_value"], "adjusted_p_value": r.get("adjusted_p_value"),
**{c: _carried(r, c) for c in _CARRIED_DEFAULTS},
})

result = pd.DataFrame(rows)
Expand Down
Loading
Loading