Metadata-Version: 2.5
Name: rrpv
Version: 0.6.0
Summary: Transition-aware gene variation analysis for single-cell expression data
Project-URL: Homepage, https://github.com/xiyangwhu/rrpv
Project-URL: Documentation, https://github.com/xiyangwhu/rrpv#readme
Project-URL: Issues, https://github.com/xiyangwhu/rrpv/issues
Project-URL: Source, https://github.com/xiyangwhu/rrpv
Author: Xi Yang
License-Expression: BSD-3-Clause
License-File: LICENSE
Keywords: anndata,cell-state-transition,gene-variation,scanpy,single-cell
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Requires-Python: >=3.10
Requires-Dist: anndata>=0.10
Requires-Dist: matplotlib>=3.7
Requires-Dist: numpy>=1.24
Requires-Dist: pandas>=2.0
Requires-Dist: scikit-learn>=1.3
Requires-Dist: scipy>=1.10
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Requires-Dist: twine>=5; extra == 'dev'
Provides-Extra: scanpy
Requires-Dist: scanpy>=1.10; extra == 'scanpy'
Description-Content-Type: text/markdown

# RRPV

RRPV (Reliability-Regularized Projector Variation) analyzes local gene-variation
geometry from a single-cell count or continuous expression matrix. It provides four
connected workflows:

- a label-free gene-variation representation for Scanpy-compatible neighborhood analysis;
- rooted graph-geodesic pseudotime on the fitted transition geometry;
- annotation-conditioned transition localization from user-supplied states;
- transition-associated gene ranking and competing-branch comparison.

Cell annotations are never used during geometry fitting. Source and target states are
introduced only in the downstream transition query, so one fitted model can support
multiple biological questions.

## Installation

```bash
pip install rrpv
```

Install the optional Scanpy dependency with:

```bash
pip install "rrpv[scanpy]"
```

## Quick start

```python
import scanpy as sc
import rrpv as rp

adata = sc.read_h5ad("counts.h5ad")

# Filter -> median-library normalization -> log1p -> 2,000 HVGs.
adata = rp.pp.preprocess(adata, copy=True)

# Label-free fit on the supplied continuous expression matrix. Its values are not
# normalized or transformed again. Automatic smoothing uses tangent-neighbor splits.
rp.tl.fit(
    adata,
    layer=None,
    input_type="expression",
    key_added="rrpv",
)

# Use the primary GVA representation in a standard Scanpy workflow.
sc.pp.neighbors(adata, use_rep="X_rrpv", key_added="rrpv_neighbors")
sc.tl.umap(adata, neighbors_key="rrpv_neighbors")
```

For raw UMI data, preserve or select the integer layer explicitly to use binomial
count-split reliability:

```python
rp.tl.fit(adata, layer="counts", input_type="counts")
```

`input_type="auto"` is the default: a nonnegative integer matrix is treated as counts;
other finite matrices are treated as continuous expression. Set the type explicitly for
integer-valued transformed assays. Continuous input may be normalized log-expression,
Pearson residuals, TPM/CPM, or another already prepared numerical assay. RRPV does not
apply a count observation model to such input.

## Rooted pseudotime

Fit the geometry first, then supply a root state to orient its distance graph:

```python
rp.tl.pseudotime(
    adata,
    groupby="cell_state",
    root="progenitor",
    key_added="rrpv_pseudotime",
)

sc.pl.umap(adata, color="rrpv_pseudotime")
```

Multiple root categories or an explicit list/boolean mask of root cells are accepted.
Use `mask_obs` to restrict the calculation to a lineage or connected analysis domain.
Cells in components without a root receive `NaN` and are marked false in
`adata.obs["rrpv_pseudotime_reachable"]`.

The estimator is the multi-source shortest-path distance on
`adata.obsp["rrpv_distances"]`. It does not reduce a branching graph to radial distance
in a low-dimensional display. The root defines orientation after the graph has been
fitted; static counts alone do not identify an arrow of time.

For gene-variation analysis without transition topology, skip the Bures chart and graph:

```python
rp.tl.fit(
    adata,
    layer="counts",
    gene_mask="highly_variable",
    compute_bures=False,
)
rp.tl.gene_variation(adata, model_key="rrpv", calibrated=True)
```

## Annotation-conditioned analysis

After fitting, define a source and target using any categorical observation column.
The states may be Leiden clusters, curated annotations, reference-mapped labels, or
another user-supplied state definition:

```python
rp.tl.transition(
    adata,
    groupby="cell_state",
    source="state_a",
    target="state_b",
    boundary_mode="core",
    model_key="rrpv",
    key_added="state_a_to_b",
)

rp.tl.rank_transition_drivers(
    adata,
    transition_key="state_a_to_b",
    model_key="rrpv",
    key_added="drivers_state_a_to_b",
)

result = rp.get.rank_transition_drivers_df(adata, key="drivers_state_a_to_b")
print(result.head(20))
```

When the sign of change along the supplied source-to-target orientation matters, build
the committor-gradient tangent field and project genes onto it:

```python
rp.tl.transition_direction(
    adata,
    transition_key="state_a_to_b",
)
rp.tl.rank_directional_genes(
    adata,
    transition_key="state_a_to_b",
    key_added="directional_state_a_to_b",
)

directional = rp.get.rank_directional_genes_df(
    adata,
    key="directional_state_a_to_b",
)
print(directional.head(20))
```

`rank_directional_genes` also creates the tangent field automatically when it is absent.
Its positive and negative projections respectively denote genes increasing and decreasing
along the supplied A-to-B orientation. Their sum measures directional magnitude without
cancelling a gene that reverses sign within the transition. The score uses neither source
nor target expression means.

By default, only the high within-state graph-retention mode of each supplied endpoint is
fixed at committor zero or one. Endpoint-edge cells and every non-endpoint cell remain
interior to the boundary-value problem. Use `boundary_mode="all"` only to reproduce the
former whole-cluster boundaries. Stored boolean columns identify the exact source and
target boundary cells.

The candidate ranking uses gene variation only. It retains committor localization but
removes endpoint expression from the score. Endpoint expression change is returned as
an annotation and never gates or changes the ranking. The legacy endpoint-conjunction
score remains available as `rp.tl.rank_transition_genes` for reproducibility.

Intermediate cells must have been measured: endpoint-only data cannot identify an
intermediate program. A single A-to-B query prioritizes transition-associated candidates,
not causal or branch-specific drivers. For a fate-choice question, fit both A-to-B and
A-to-C queries and contrast their rankings:

```python
rp.tl.branch_contrast(
    adata,
    ranking_a="drivers_state_a_to_b",
    ranking_b="drivers_state_a_to_c",
    key_added="state_b_vs_state_c",
)
```

## Optional graph-state exploration

Graph-derived states can be explored when no annotation is available, but this optional
module is not the default transition workflow and does not establish biological
transition states:

```python
# Label-free selection over a scientifically relevant resolution range.
rp.tl.discover_states(
    adata,
    n_states="auto",
    min_states=9,
    max_states=15,
)

states = rp.get.discovered_states_df(adata)
selection = rp.get.state_count_selection_df(adata)
rp.pl.transition_atlas(adata, basis="X_umap")
```

Automatic state count evaluates local peaks of the Markov relaxation-rate separation
inside `[min_states, max_states]`. For every candidate, RRPV computes the geometric mean
of ARI and AMI between edge-weight multiplier-bootstrap partitions, then subtracts a
degree-preserving rewiring null baseline. The finest candidate within one standard error
of the maximum calibrated score is selected. These ARI/AMI values compare label-free
graph views, not cell-type annotations. `n_states="coarse"` retains the strongest single
relaxation separation when a macrostate overview is wanted.

Existing annotations can instead define an exploratory state skeleton exactly:

```python
rp.tl.discover_states(adata, cluster_key="leiden")
rp.pl.transition_atlas(adata, basis="X_umap")
```

The supplied labels are copied without reclustering and the result records
`labels_or_time_used=True`. Automated pairwise atlas and reactive-cluster functions
remain available for research reproducibility, but they are experimental: enumerating a
committor between two states is not evidence that the corresponding biological
transition occurs. The supported primary workflow is the explicit
`transition(groupby, source, target)` query above.

## Coherent two-resolution geometry

RRPV fits one set of local tangent frames and one state-space diffusion operator. Two
resolutions are then used for different estimands:

1. A stable `q`-dimensional projector field defines `X_rrpv`, split-half reliability,
   and the optional Bures transition topology.
2. Full-dimensional tangent-frame contractions retain gene information that would be
   lost by truncating every gene score to `q` dimensions.

Both resolutions share the same counts, global basis, neighborhoods, local rank,
diffusion operator, and selected diffusion time. The distinction is explicit in the
stored metadata: `geometry_dimension` records `q`, while `gene_variation_dimension`
records the full latent dimension.

The automatic selector evaluates exactly the normalized geometry produced by the final
fit:

\[
G_i^{(t)}=(D^tG^{(0)})_i,
\qquad
\Sigma_i^{(t)}=
\frac{G_i^{(t)}}{\operatorname{tr}(G_i^{(t)})}.
\]

For counts, binomial split halves change only the count realization. For continuous
expression, two disjoint halves of every tangent neighborhood provide geometric
reliability views while the global coordinates and graph remain fixed. In both cases,
the earliest diffusion time within one standard error of maximum centered-projector
reliability is selected.

The full mathematical definition is provided in the
[method specification](https://github.com/xiyangwhu/rrpv/blob/main/docs/method.md).

## Gene variation and leverage

For the full loading vector \(b_g\), local frame \(F_i\), and common local rank \(r\),
RRPV computes

\[
a_{ig}^{(0)}=\frac{\lVert F_i b_g\rVert_2^2}{r},
\qquad
v_{ig}=\sqrt{(D^{t^\ast}a_g^{(0)})_i}.
\]

Raw gene variation contains a representability factor
\(\ell_g=\lVert b_g\rVert_2^2\). For cross-gene comparisons,
`rp.tl.gene_variation(..., calibrated=True)` returns

\[
\widetilde v_{ig}=\frac{v_{ig}}{\sqrt{\ell_g+\varepsilon}}.
\]

Genes whose leverage is below the numerical identifiability floor are marked in
`adata.var["rrpv_leverage_identifiable"]` and receive `NaN` calibrated values. They are
excluded from transition ranking instead of being converted to artificial finite scores.

## Scanpy-compatible outputs

| Result | AnnData location |
|---|---|
| Primary square-root diagonal GVA representation | `adata.obsm["X_rrpv"]` |
| Explicit primary alias | `adata.obsm["X_rrpv_diagonal"]` |
| Optional Bures root chart | `adata.obsm["X_rrpv_bures"]` |
| Optional transition connectivity | `adata.obsp["rrpv_connectivities"]` |
| Global cell-state coordinates | `adata.obsm["X_rrpv_state"]` |
| Full local tangent frames | `adata.obsm["rrpv_local_frames"]` |
| Full gene loadings | `adata.varm["rrpv_loadings"]` |
| Gene leverage and identifiability | `adata.var["rrpv_leverage"]`, `adata.var["rrpv_leverage_identifiable"]` |
| Materialized calibrated variation | `adata.layers["rrpv_gene_variation"]` |
| Rooted pseudotime and reachability | `adata.obs["rrpv_pseudotime"]`, `adata.obs["rrpv_pseudotime_reachable"]` |
| Optional graph-derived states | `adata.obs["rrpv_states_state"]`, `adata.uns["rrpv_states"]` |
| Committor and transition weight | `adata.obs["<key>_committor"]`, `adata.obs["<key>_weight"]` |
| Auditable endpoint-core boundaries | `adata.obs["<key>_source_boundary"]`, `adata.obs["<key>_target_boundary"]` |
| Diffused A-to-B tangent field | `adata.obsm["X_<key>_direction_tangent"]` |
| Signed directional gene ranking | `adata.uns["rank_<key>_directional"]` |
| Variation-only candidate ranking | `adata.uns["rank_<key>_drivers"]` |
| Legacy endpoint-conjunction ranking | `adata.uns["rank_<key>"]` |
| Model definition and parameters | `adata.uns["rrpv"]` |

## Visualization

```python
rp.pl.diffusion_reliability(adata)
rp.pl.transition(adata, transition_key="state_a_to_b", basis="X_umap")
rp.pl.transition_quiver(adata, transition_key="state_a_to_b", basis="X_umap")
rp.pl.transition_stream(adata, transition_key="state_a_to_b", basis="X_umap")
rp.pl.transition_diagnostics(adata, transition_key="state_a_to_b", basis="X_umap")
rp.pl.gene_trends(
    adata,
    genes=["GENE_A", "GENE_B"],
    transition_key="state_a_to_b",
)
```

Quiver and stream plots display the positive local gradient of the fitted graph
committor. They are not RNA-velocity vectors and do not establish temporal or causal
direction.

## Interpretation

RRPV answers five descriptive questions:

- Which cells have similar local gene-variation geometry?
- How far is each reachable cell from a supplied root along that geometry?
- Which cells lie near a user-annotation-conditioned boundary on that geometry?
- Which genes show localized variation along a supplied endpoint query or differ between
  competing branches?
- Which genes are aligned or anti-aligned with the source-to-target committor gradient?

Static counts do not identify lineage, arrow of time, or causal regulation. Those claims
require perturbation, lineage tracing, time-resolved sampling, or independent validation.

See the [version migration guide](https://github.com/xiyangwhu/rrpv/blob/main/docs/migration.md)
before reusing fitted objects from an older release.

## Citation and license

Citation metadata are provided in
[`CITATION.cff`](https://github.com/xiyangwhu/rrpv/blob/main/CITATION.cff). RRPV is
distributed under the BSD-3-Clause license.
