Metadata-Version: 2.4
Name: scfair
Version: 0.8.0
Summary: HVG selection for scRNA-seq: structure-aware auto list size (n) plus same-rank append at the cutoff, with diagnostics
Author-email: "Zhao Li (李钊)" <leelieber@gmail.com>
License: Apache-2.0
Project-URL: Homepage, https://github.com/leelieber2025/scFair
Project-URL: Documentation, https://scfair.readthedocs.io
Project-URL: Changelog, https://scfair.readthedocs.io/en/latest/changelog.html
Project-URL: Source, https://github.com/leelieber2025/scFair
Project-URL: Issues, https://github.com/leelieber2025/scFair/issues
Keywords: single-cell,RNA-seq,HVG,highly-variable-genes,fairness,bioinformatics
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: anndata>=0.10
Requires-Dist: numpy>=1.24
Requires-Dist: pandas>=2.0
Requires-Dist: scanpy>=1.10
Requires-Dist: scipy>=1.10
Requires-Dist: scikit-learn>=1.2
Requires-Dist: igraph>=0.10
Requires-Dist: scikit-misc>=0.3
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: ruff<0.17,>=0.4.0; extra == "dev"
Provides-Extra: docs
Requires-Dist: sphinx>=7.0; extra == "docs"
Requires-Dist: sphinx-book-theme>=1.1; extra == "docs"
Requires-Dist: myst-nb>=1.0; extra == "docs"
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
Requires-Dist: sphinx-design>=0.5; extra == "docs"
Dynamic: license-file

# scFair

[![PyPI version](https://img.shields.io/pypi/v/scfair.svg)](https://pypi.org/project/scfair/)
[![Python versions](https://img.shields.io/pypi/pyversions/scfair.svg)](https://pypi.org/project/scfair/)
[![Documentation Status](https://readthedocs.org/projects/scfair/badge/?version=latest)](https://scfair.readthedocs.io/en/latest/?badge=latest)
[![CI](https://github.com/leelieber2025/scFair/actions/workflows/tests.yml/badge.svg)](https://github.com/leelieber2025/scFair/actions/workflows/tests.yml)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)

**scFair** is drop-in HVG selection for single-cell RNA-seq aimed at two
problems every pipeline hits — plus an optional density population count:

| Problem | What goes wrong | What scFair does |
|---------|-----------------|------------------|
| **How many genes (`n`)?** | Nobody knows a priori what length is safe. Copying `2000` is a silent gamble. | **`n_top_genes="auto"`** (**default**) estimates a base size from density structure. That multi-seed cost is intentional: **avoiding a wrong `n` is worth the compute**. Pass a fixed int only when the protocol is already locked. |
| **Unfair HVG allocation at the cutoff** | Global ranking is driven by large populations. Markers for smaller types often sit **just below** a hard top-`k` cut and are discarded — not because they are uninformative, but because bulk variation fills the list first. | Default **`append`** freezes the global top-`k` backbone and adds a short **same-rank** tail (`append_budget`) so near-miss genes are not thrown away. The set equals **`top-(k+m)`**. This is a **conservative response to cutoff unfairness**, not cluster-conditional reallocation (those methods were removed). Use `balance_method="none"` for pure top-`k`. |

Optional: **`estimate_n_populations`** reads a 3D density field after
`sc.pp.neighbors` (how many populations, without sweeping Leiden resolution;
works best up to ~20 well-separated populations).

Docs: [Read the Docs](https://scfair.readthedocs.io/en/latest/).

## What you need

- Python 3.10+ (tested 3.10–3.12)
- An AnnData object with raw integer counts in `.X` or `layers["counts"]`

## Install

```bash
pip install scfair
```

## First run

```python
import scanpy as sc
import scfair as scf

# raw integer counts in .X or layers["counts"]
scf.pp.highly_variable_genes(adata)
# annotates adata.var["highly_variable"]; keep the full gene matrix
# (scanpy PCA uses that mask by default — no manual subset step)

sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
sc.pp.scale(adata, max_value=10)
sc.tl.pca(adata)
sc.pp.neighbors(adata)
sc.tl.leiden(adata, flavor="igraph", n_iterations=2, directed=False)
```

Default path: structure auto-`n` + same-rank **append** (cutoff buffer). Auto
runs extra multi-seed graph builds and is slower than a fixed `k` — that is
by design. **A silent wrong list length is more expensive than the compute.**
Read `adata.uns["scfair"]["hvg"]["auto_message"]` for a one-line summary of the
chosen base size. Use a fixed int only for a locked paper/protocol:

```python
scf.pp.highly_variable_genes(adata, n_top_genes=2000)
```

Pure top-`k` (no buffer):

```python
scf.pp.highly_variable_genes(adata, n_top_genes=2000, balance_method="none")
```

Population count without resolution sweep (after neighbors):

```python
sc.pp.neighbors(adata)
est = scf.pp.estimate_n_populations(adata)
print(est.n_populations, est.confidence)
```

## Status

**0.8.0 (Beta).** Import as `import scfair as scf` and use names in
`scfair.__all__` and `scf.pp`. See the
[API reference](https://scfair.readthedocs.io/en/latest/api/index.html).

## Next steps

1. [Installation](https://scfair.readthedocs.io/en/latest/installation.html)
2. [Quickstart](https://scfair.readthedocs.io/en/latest/quickstart.html)
3. [Tutorial: PBMC 10k misclustered cells vs standard HVG](https://scfair.readthedocs.io/en/latest/tutorials/index.html)
4. [FAQ](https://scfair.readthedocs.io/en/latest/faq.html) if something looks off

## License

Software: [Apache License 2.0](LICENSE).

## Author

**Zhao Li (李钊)**  
Email: [leelieber@gmail.com](mailto:leelieber@gmail.com)
