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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
env:
UV_PYTHON: ${{ matrix.python-version }}

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Install dependencies
run: uv sync --locked --extra mcp

- name: Lint
run: uv run ruff check src tests

- name: Test
run: uv run pytest tests/ -v
61 changes: 58 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,24 @@ results = ctv.validate(
similarity_lookup="data/mappings/efo_similarity_lookup_0.5.parquet",
)
print(results)
# phase_label n_yes n_no rr rr_ci_lower rr_ci_upper ...
# phase_label n_yes n_no rr rr_ci_lower rr_ci_upper p_value ...
```

Export annotated trials and/or matched-pair audit rows (returned in order):

```python
enrichment, trials = ctv.validate(..., return_trials=True)
enrichment, matched = ctv.validate(..., return_matched_pairs=True)
enrichment, trials, matched = ctv.validate(..., return_trials=True, return_matched_pairs=True)
```

Or build the match-audit table directly (columns: `gene`, `ct_efo_id`, `ge_efo_id`, `similarity`, `match_type`):

```python
matched = ctv.create_matched_pairs_df(
genetic_evidence=..., clinical_trials=..., similarity_pairs=...,
similarity_threshold=0.8,
)
```

Batch mode — compare multiple evidence sources at once:
Expand Down Expand Up @@ -95,6 +112,10 @@ ct-validation \
--clinical-trials ct.parquet \
--targets gwas.parquet --targets clinvar.parquet --targets omim.parquet \
-o results/

# Save annotated trials and/or matched-pair audit rows
ct-validation --config configs/default.yaml \
--save-trials --save-matched-pairs -o results/
```

### MCP server
Expand All @@ -105,7 +126,7 @@ ct-validation-mcp

Exposes two tools for agent-based workflows:

- `ct_validate` — compute phase-transition enrichment
- `ct_validate` — compute phase-transition enrichment (includes `p_value`)
- `expand_disease_set` — expand EFO IDs via semantic similarity

## Input schemas
Expand All @@ -130,6 +151,21 @@ All inputs accept Parquet files or pandas DataFrames (except `gene_universe`, wh
| `rate_yes`, `rate_no` | Progression rates |
| `rr`, `rr_ci_lower`, `rr_ci_upper` | Risk ratio with 95% CI (Katz log method) |
| `or`, `or_ci_lower`, `or_ci_upper` | Odds ratio with 95% CI (Woolf logit method) |
| `p_value` | Two-sided Fisher's exact test p-value |

When either comparison group is empty (`n_yes=0` or `n_no=0`), `rr`, `or`, their confidence intervals, and `p_value` are undefined (`NaN`).

### Matched-pairs export (optional)

When `return_matched_pairs=True` (API) or `--save-matched-pairs` (CLI) is set, a separate audit table is returned/saved with:

| Column | Description |
| ------------ | -------------------------------------------------------- |
| `gene` | Gene symbol |
| `ct_efo_id` | Disease on the clinical trial row |
| `ge_efo_id` | Supporting genetic-evidence disease |
| `similarity` | Match score (`1.0` for exact matches) |
| `match_type` | `exact` when `ct_efo_id == ge_efo_id`, else `similarity` |

## Enrichment logic

Expand All @@ -139,7 +175,7 @@ For each phase transition, target-indication pairs that reached at least the sta
RR = (x_yes / n_yes) / (x_no / n_no)
```

A risk ratio greater than one indicates that genetically supported pairs are more likely to progress. When a similarity lookup is provided, a pair (gene, disease) is considered supported if there exists evidence (gene, disease') with similarity above the threshold (default 0.8).
A risk ratio greater than one indicates that genetically supported pairs are more likely to progress. When a similarity lookup is provided, a pair (gene, disease) is considered supported if there exists evidence (gene, disease') with similarity above the threshold (default 0.8). Similarity pairs may be stored in either orientation; matching searches both directions.

### Prioritized mode

Expand Down Expand Up @@ -190,6 +226,25 @@ python scripts/parse/run_parsing.py

See `configs/default.yaml` for validation settings and `configs/parsing.yaml` for data source paths. All config values can be overridden via CLI arguments.

Output options in `configs/default.yaml`:

```yaml
output:
dir: "results/"
save_trials: false
save_matched_pairs: false
```

## Development

CI runs on push/PR to `main` (Python 3.11–3.13, `ruff`, `pytest`):

```bash
uv sync --extra mcp
uv run ruff check src tests
uv run pytest tests/ -v
```

## License

MIT
2 changes: 2 additions & 0 deletions src/ct_validation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ct_validation.validation import (
EnrichmentResult,
calculate_enrichment,
create_matched_pairs_df,
create_matched_pairs_set,
get_expanded_disease_set,
katz_ci_risk_ratio,
Expand All @@ -24,6 +25,7 @@
"Config",
"EnrichmentResult",
"calculate_enrichment",
"create_matched_pairs_df",
"create_matched_pairs_set",
"forest_plot",
"get_expanded_disease_set",
Expand Down
67 changes: 55 additions & 12 deletions src/ct_validation/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ct_validation.config.schema import DEFAULT_PHASE_TRANSITIONS, Config, Thresholds
from ct_validation.data.schema import CLINICAL_TRIALS, GENETIC_EVIDENCE, SIMILARITY_LOOKUP
from ct_validation.validation.enrichment import calculate_all_enrichments
from ct_validation.validation.matching import create_matched_pairs_set
from ct_validation.validation.matching import create_matched_pairs_df, create_matched_pairs_set

_DataInput = pd.DataFrame | str | Path

Expand Down Expand Up @@ -142,6 +142,25 @@ def _prepare_clinical_trials(
return ct.drop(columns=["_has_baseline"])


def _pack_validation_result(
enrichment: pd.DataFrame,
trials: pd.DataFrame | None,
matched_pairs: pd.DataFrame | None,
*,
return_trials: bool,
return_matched_pairs: bool,
) -> pd.DataFrame | tuple:
"""Build validate() return value from optional extras."""
if not return_trials and not return_matched_pairs:
return enrichment
parts: list[pd.DataFrame] = [enrichment]
if return_trials:
parts.append(trials)
if return_matched_pairs:
parts.append(matched_pairs)
return tuple(parts)


def validate(
config: Config | str | Path | None = None,
*,
Expand All @@ -153,7 +172,8 @@ def validate(
similarity_threshold: float | None = None,
phase_transitions: list[tuple[int, int]] | None = None,
return_trials: bool = False,
) -> pd.DataFrame | tuple[pd.DataFrame, pd.DataFrame] | list:
return_matched_pairs: bool = False,
) -> pd.DataFrame | tuple | list:
"""
Run validation pipeline.

Expand Down Expand Up @@ -191,9 +211,12 @@ def validate(
rate_yes, rate_no: Progression rates.
rr, rr_ci_lower, rr_ci_upper: Risk ratio with 95% CI (Katz log method).
or, or_ci_lower, or_ci_upper: Odds ratio with 95% CI (Woolf logit method).
p_value: Two-sided Fisher's exact test p-value.

Returns:
Single targets: DataFrame (or tuple with trials_df if return_trials=True).
Single targets: enrichment DataFrame, or tuple with optional extras in order:
(enrichment), (enrichment, trials), (enrichment, matched_pairs),
or (enrichment, trials, matched_pairs).
List of targets: list of the above, one per target set.
"""
batch = isinstance(targets, list)
Expand Down Expand Up @@ -238,14 +261,26 @@ def validate(

results = []
for t in p.targets_list:
target_pairs = create_matched_pairs_set(
genetic_evidence=t,
clinical_trials=p.clinical_trials,
similarity_pairs=p.similarity_lookup,
similarity_threshold=p.similarity_threshold,
gene_universe=gu,
con=con,
)
if return_matched_pairs:
matched_pairs_df = create_matched_pairs_df(
genetic_evidence=t,
clinical_trials=p.clinical_trials,
similarity_pairs=p.similarity_lookup,
similarity_threshold=p.similarity_threshold,
gene_universe=gu,
con=con,
)
target_pairs = set(zip(matched_pairs_df["gene"], matched_pairs_df["ct_efo_id"]))
else:
matched_pairs_df = None
target_pairs = create_matched_pairs_set(
genetic_evidence=t,
clinical_trials=p.clinical_trials,
similarity_pairs=p.similarity_lookup,
similarity_threshold=p.similarity_threshold,
gene_universe=gu,
con=con,
)

ct = _prepare_clinical_trials(
clinical_trials=p.clinical_trials,
Expand All @@ -259,7 +294,15 @@ def validate(
phase_transitions=p.phase_transitions,
)

results.append((enrichment, ct) if return_trials else enrichment)
results.append(
_pack_validation_result(
enrichment,
ct,
matched_pairs_df,
return_trials=return_trials,
return_matched_pairs=return_matched_pairs,
),
)

con.close()

Expand Down
Loading
Loading