scbamqc 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
scbamqc-0.1.0/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026 Argenis Arriojas
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
scbamqc-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: scbamqc
3
+ Version: 0.1.0
4
+ Summary: Per-barcode BAM alignment QC and alignment-based cell filtering for single-cell RNA-seq
5
+ Author-email: Argenis Arriojas <ArriojasMaldonado001@umb.edu>
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Classifier: License :: OSI Approved :: BSD License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
15
+ License-File: LICENSE
16
+ Requires-Dist: cyclopts>=3
17
+ Requires-Dist: numpy
18
+ Requires-Dist: pandas
19
+ Requires-Dist: rich
20
+ Requires-Dist: scikit-learn
21
+ Requires-Dist: scipy
22
+ Requires-Dist: scbamqc[anndata, bam, plot] ; extra == "all"
23
+ Requires-Dist: anndata>=0.10 ; extra == "anndata"
24
+ Requires-Dist: h5py ; extra == "anndata"
25
+ Requires-Dist: pysam>=0.22 ; extra == "bam"
26
+ Requires-Dist: matplotlib ; extra == "plot"
27
+ Requires-Dist: pytest>=8.3.4 ; extra == "test"
28
+ Project-URL: Bug Tracker, https://github.com/umbibio/scbamqc/issues
29
+ Project-URL: Changelog, https://github.com/umbibio/scbamqc/blob/main/CHANGELOG.md
30
+ Project-URL: Homepage, https://github.com/umbibio/scbamqc
31
+ Project-URL: Repository, https://github.com/umbibio/scbamqc.git
32
+ Provides-Extra: all
33
+ Provides-Extra: anndata
34
+ Provides-Extra: bam
35
+ Provides-Extra: plot
36
+ Provides-Extra: test
37
+
38
+ # scbamqc
39
+
40
+ Alignment QC for single-cell BAM files, and cell selection from that QC.
41
+
42
+ - `scbamqc bam` writes one row of alignment statistics for each cell barcode.
43
+ - `scbamqc.qc` calculates a quality score for each barcode and selects cells.
44
+ - `scbamqc.pl` plots the statistics and the score.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install scbamqc
50
+ ```
51
+
52
+ Add extras for more functions:
53
+
54
+ | Extra | Adds | Use it for |
55
+ |---|---|---|
56
+ | `bam` | pysam | `scbamqc bam` and `scbamqc tags` (Linux, macOS) |
57
+ | `anndata` | anndata, h5py | `scbamqc.io.read_10x_h5` |
58
+ | `plot` | matplotlib | `scbamqc.pl` and `scbamqc bam --plots` |
59
+ | `all` | all of the above | |
60
+
61
+ For example: `pip install "scbamqc[bam,plot]"`.
62
+
63
+ ## Command line
64
+
65
+ The BAM file must be sorted by coordinate and indexed (`samtools index`).
66
+
67
+ ```bash
68
+ # Cell Ranger
69
+ scbamqc bam possorted_genome_bam.bam -j 16 --out-prefix S1
70
+
71
+ # STARsolo SmartSeq: one read group for each cell
72
+ scbamqc bam Aligned.sortedByCoord.out.bam --barcode-tag RG
73
+ ```
74
+
75
+ The tool writes `S1_per_cell.csv.gz`, with one row for each barcode. To also
76
+ write histograms to `S1_plots.png`, add `--plots`.
77
+
78
+ For all options, run `scbamqc bam --help`.
79
+
80
+ ### Other pipelines
81
+
82
+ Different pipelines write the barcode and the mismatch count in different BAM
83
+ tags. Find the tags before you run `bam`:
84
+
85
+ ```bash
86
+ scbamqc tags sample.bam
87
+ scbamqc bam sample.bam --barcode-tag CB --nm-tag NM
88
+ ```
89
+
90
+ `bam` shows a warning if most reads do not have the tags you set.
91
+
92
+ > [!WARNING]
93
+ > Without a mismatch tag, `mean_nm` is empty (`NaN`). Then no barcode gets a
94
+ > quality score.
95
+
96
+ ## Python
97
+
98
+ ```python
99
+ import scbamqc as sbq
100
+ from scbamqc.io import read_10x_h5
101
+
102
+ adata = read_10x_h5("raw_feature_bc_matrix.h5")
103
+ adata = sbq.add_bam_qc(adata, "S1_per_cell.csv.gz")
104
+ adata = sbq.basic_qc(adata)
105
+ sbq.qc_score(adata, min_reads=150)
106
+ sbq.select_cells(adata)
107
+
108
+ sbq.pl.qc_score_scatter(adata, min_score=0, min_counts=100)
109
+ sbq.pl.qc_score_hist(adata, threshold=0.9)
110
+
111
+ cells = adata[adata.obs["selected"] & (adata.obs["qc_score"] > 0.9)].copy()
112
+ ```
113
+
114
+ | Step | Result |
115
+ |---|---|
116
+ | `add_bam_qc` | Keeps the barcodes in the BAM table. Adds the table columns to `obs`. |
117
+ | `basic_qc` | Removes barcodes with fewer than 5 genes and genes in fewer than 5 barcodes. Adds `total_counts`. |
118
+ | `qc_score` | Adds `obs["qc_score"]`, from 0 to 1. |
119
+ | `select_cells` | Adds `obs["gmm_label"]` and `obs["selected"]`. |
120
+
121
+ ### The quality score
122
+
123
+ 1. A barcode gets a score only if `n_reads > min_reads` and `mean_nm < 1`.
124
+ Other barcodes get `NaN`.
125
+ 2. A kernel density is fitted to three values of these barcodes:
126
+ `mean_clip_frac`, `std_clip_frac` and `mean_base_qual` divided by its
127
+ median.
128
+ 3. The score is `log10(density + 1)`, scaled from 0 to 1.
129
+
130
+ A high score shows that the barcode aligns like most other barcodes.
131
+
132
+ Set `min_reads` for each library. The paper used values from 50 to 3,000.
133
+
134
+ `base_qual_scale` sets the divisor of `mean_base_qual`. The default is the
135
+ median of the scored barcodes. The divisor does not change the order of the
136
+ barcodes. It can change the score values only when the densities are near 1.
137
+
138
+ To use the same settings as the TgRN paper:
139
+
140
+ ```python
141
+ sbq.qc_score(adata, min_reads=150, base_qual_scale=sbq.PAPER_BASE_QUAL_SCALE)
142
+ sbq.select_cells(adata, tol=sbq.PAPER_GMM_TOL)
143
+ ```
144
+
145
+ ### Cell selection
146
+
147
+ `select_cells` fits a Gaussian mixture with two components to
148
+ `log10(total_counts)` and `qc_score`. A barcode is selected if:
149
+
150
+ - it is in the component with the higher counts,
151
+ - `total_counts > min_counts` (default 100),
152
+ - `qc_score > min_score` (default 0).
153
+
154
+ The mixture fit runs until it converges. The paper stopped the fit early
155
+ (`tol=PAPER_GMM_TOL`). On the paper sample, the converged fit selects 15,536
156
+ cells and the paper fit selects 16,120. All cells of the converged fit are in
157
+ the paper set.
158
+
159
+ ## R
160
+
161
+ The R package in [`r/`](r/README.md) does the same steps for Seurat objects.
162
+ It reads the table that `scbamqc bam` writes.
163
+
164
+ ```r
165
+ remotes::install_github("umbibio/scbamqc", subdir = "r")
166
+ ```
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ uv sync
172
+ uv run pytest
173
+ ```
174
+
175
+ The tests in `tests/test_paper.py` compare the results with the paper. They
176
+ need the data in `../../data`, or in the folder set by `SCBAMQC_TEST_DATA`.
177
+ Without the data, pytest skips them.
178
+
179
+ ## License
180
+
181
+ BSD 3-Clause.
182
+
@@ -0,0 +1,144 @@
1
+ # scbamqc
2
+
3
+ Alignment QC for single-cell BAM files, and cell selection from that QC.
4
+
5
+ - `scbamqc bam` writes one row of alignment statistics for each cell barcode.
6
+ - `scbamqc.qc` calculates a quality score for each barcode and selects cells.
7
+ - `scbamqc.pl` plots the statistics and the score.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install scbamqc
13
+ ```
14
+
15
+ Add extras for more functions:
16
+
17
+ | Extra | Adds | Use it for |
18
+ |---|---|---|
19
+ | `bam` | pysam | `scbamqc bam` and `scbamqc tags` (Linux, macOS) |
20
+ | `anndata` | anndata, h5py | `scbamqc.io.read_10x_h5` |
21
+ | `plot` | matplotlib | `scbamqc.pl` and `scbamqc bam --plots` |
22
+ | `all` | all of the above | |
23
+
24
+ For example: `pip install "scbamqc[bam,plot]"`.
25
+
26
+ ## Command line
27
+
28
+ The BAM file must be sorted by coordinate and indexed (`samtools index`).
29
+
30
+ ```bash
31
+ # Cell Ranger
32
+ scbamqc bam possorted_genome_bam.bam -j 16 --out-prefix S1
33
+
34
+ # STARsolo SmartSeq: one read group for each cell
35
+ scbamqc bam Aligned.sortedByCoord.out.bam --barcode-tag RG
36
+ ```
37
+
38
+ The tool writes `S1_per_cell.csv.gz`, with one row for each barcode. To also
39
+ write histograms to `S1_plots.png`, add `--plots`.
40
+
41
+ For all options, run `scbamqc bam --help`.
42
+
43
+ ### Other pipelines
44
+
45
+ Different pipelines write the barcode and the mismatch count in different BAM
46
+ tags. Find the tags before you run `bam`:
47
+
48
+ ```bash
49
+ scbamqc tags sample.bam
50
+ scbamqc bam sample.bam --barcode-tag CB --nm-tag NM
51
+ ```
52
+
53
+ `bam` shows a warning if most reads do not have the tags you set.
54
+
55
+ > [!WARNING]
56
+ > Without a mismatch tag, `mean_nm` is empty (`NaN`). Then no barcode gets a
57
+ > quality score.
58
+
59
+ ## Python
60
+
61
+ ```python
62
+ import scbamqc as sbq
63
+ from scbamqc.io import read_10x_h5
64
+
65
+ adata = read_10x_h5("raw_feature_bc_matrix.h5")
66
+ adata = sbq.add_bam_qc(adata, "S1_per_cell.csv.gz")
67
+ adata = sbq.basic_qc(adata)
68
+ sbq.qc_score(adata, min_reads=150)
69
+ sbq.select_cells(adata)
70
+
71
+ sbq.pl.qc_score_scatter(adata, min_score=0, min_counts=100)
72
+ sbq.pl.qc_score_hist(adata, threshold=0.9)
73
+
74
+ cells = adata[adata.obs["selected"] & (adata.obs["qc_score"] > 0.9)].copy()
75
+ ```
76
+
77
+ | Step | Result |
78
+ |---|---|
79
+ | `add_bam_qc` | Keeps the barcodes in the BAM table. Adds the table columns to `obs`. |
80
+ | `basic_qc` | Removes barcodes with fewer than 5 genes and genes in fewer than 5 barcodes. Adds `total_counts`. |
81
+ | `qc_score` | Adds `obs["qc_score"]`, from 0 to 1. |
82
+ | `select_cells` | Adds `obs["gmm_label"]` and `obs["selected"]`. |
83
+
84
+ ### The quality score
85
+
86
+ 1. A barcode gets a score only if `n_reads > min_reads` and `mean_nm < 1`.
87
+ Other barcodes get `NaN`.
88
+ 2. A kernel density is fitted to three values of these barcodes:
89
+ `mean_clip_frac`, `std_clip_frac` and `mean_base_qual` divided by its
90
+ median.
91
+ 3. The score is `log10(density + 1)`, scaled from 0 to 1.
92
+
93
+ A high score shows that the barcode aligns like most other barcodes.
94
+
95
+ Set `min_reads` for each library. The paper used values from 50 to 3,000.
96
+
97
+ `base_qual_scale` sets the divisor of `mean_base_qual`. The default is the
98
+ median of the scored barcodes. The divisor does not change the order of the
99
+ barcodes. It can change the score values only when the densities are near 1.
100
+
101
+ To use the same settings as the TgRN paper:
102
+
103
+ ```python
104
+ sbq.qc_score(adata, min_reads=150, base_qual_scale=sbq.PAPER_BASE_QUAL_SCALE)
105
+ sbq.select_cells(adata, tol=sbq.PAPER_GMM_TOL)
106
+ ```
107
+
108
+ ### Cell selection
109
+
110
+ `select_cells` fits a Gaussian mixture with two components to
111
+ `log10(total_counts)` and `qc_score`. A barcode is selected if:
112
+
113
+ - it is in the component with the higher counts,
114
+ - `total_counts > min_counts` (default 100),
115
+ - `qc_score > min_score` (default 0).
116
+
117
+ The mixture fit runs until it converges. The paper stopped the fit early
118
+ (`tol=PAPER_GMM_TOL`). On the paper sample, the converged fit selects 15,536
119
+ cells and the paper fit selects 16,120. All cells of the converged fit are in
120
+ the paper set.
121
+
122
+ ## R
123
+
124
+ The R package in [`r/`](r/README.md) does the same steps for Seurat objects.
125
+ It reads the table that `scbamqc bam` writes.
126
+
127
+ ```r
128
+ remotes::install_github("umbibio/scbamqc", subdir = "r")
129
+ ```
130
+
131
+ ## Development
132
+
133
+ ```bash
134
+ uv sync
135
+ uv run pytest
136
+ ```
137
+
138
+ The tests in `tests/test_paper.py` compare the results with the paper. They
139
+ need the data in `../../data`, or in the folder set by `SCBAMQC_TEST_DATA`.
140
+ Without the data, pytest skips them.
141
+
142
+ ## License
143
+
144
+ BSD 3-Clause.
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "scbamqc"
3
+ version = "0.1.0"
4
+ description = "Per-barcode BAM alignment QC and alignment-based cell filtering for single-cell RNA-seq"
5
+ readme = "README.md"
6
+ license = { file = "LICENSE" }
7
+ authors = [
8
+ { name = "Argenis Arriojas", email = "ArriojasMaldonado001@umb.edu" },
9
+ ]
10
+ requires-python = ">=3.11"
11
+ classifiers = [
12
+ "License :: OSI Approved :: BSD License",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
19
+ ]
20
+ dependencies = [
21
+ "cyclopts>=3",
22
+ "numpy",
23
+ "pandas",
24
+ "rich",
25
+ "scikit-learn",
26
+ "scipy",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ anndata = ["anndata>=0.10", "h5py"]
31
+ bam = ["pysam>=0.22"]
32
+ plot = ["matplotlib"]
33
+ all = ["scbamqc[anndata,bam,plot]"]
34
+ test = ["pytest>=8.3.4"]
35
+
36
+ [project.scripts]
37
+ scbamqc = "scbamqc.cli:main"
38
+
39
+ [project.urls]
40
+ "Homepage" = "https://github.com/umbibio/scbamqc"
41
+ "Repository" = "https://github.com/umbibio/scbamqc.git"
42
+ "Bug Tracker" = "https://github.com/umbibio/scbamqc/issues"
43
+ "Changelog" = "https://github.com/umbibio/scbamqc/blob/main/CHANGELOG.md"
44
+
45
+ [build-system]
46
+ requires = ["flit_core>=3.2,<4"]
47
+ build-backend = "flit_core.buildapi"
48
+
49
+ [tool.flit.module]
50
+ name = "scbamqc"
51
+
52
+ [tool.flit.sdist]
53
+ exclude = ["r/"]
54
+
55
+ [dependency-groups]
56
+ dev = [
57
+ "bump-my-version>=0.31.1",
58
+ "pre-commit>=4.1.0",
59
+ "ruff>=0.9.4",
60
+ "scbamqc[test,all]",
61
+ ]
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
65
+ markers = [
66
+ "paper: regression tests against the paper's local data (skipped when absent)",
67
+ ]
@@ -0,0 +1,30 @@
1
+ """Per-barcode BAM alignment QC and alignment-based cell filtering."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from . import pl, qc
6
+ from .qc import (
7
+ DEFAULT_FEATURES,
8
+ PAPER_BASE_QUAL_SCALE,
9
+ PAPER_GMM_TOL,
10
+ add_bam_qc,
11
+ basic_qc,
12
+ gmm_labels,
13
+ qc_score,
14
+ read_bam_qc,
15
+ select_cells,
16
+ )
17
+
18
+ __all__ = [
19
+ "DEFAULT_FEATURES",
20
+ "PAPER_BASE_QUAL_SCALE",
21
+ "PAPER_GMM_TOL",
22
+ "add_bam_qc",
23
+ "basic_qc",
24
+ "gmm_labels",
25
+ "pl",
26
+ "qc",
27
+ "qc_score",
28
+ "read_bam_qc",
29
+ "select_cells",
30
+ ]
@@ -0,0 +1,79 @@
1
+ """Terminal summary of a BAM-QC run (``rich``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pandas as pd
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+
10
+ from .bam import NO_BARCODE, CellStats, StatsKey
11
+ from .pl import _id_label
12
+
13
+
14
+ def _pct(part: int, total: int) -> str:
15
+ return f"{100 * part / total:.1f}%" if total else "n/a"
16
+
17
+
18
+ def print_summary(
19
+ console: Console,
20
+ df: pd.DataFrame,
21
+ stats: dict[StatsKey, CellStats],
22
+ barcode_tag: str = "CB",
23
+ group_by: str = "cell",
24
+ ) -> None:
25
+ total_reads = sum(cs.n_reads for cs in stats.values())
26
+ no_cb_reads = sum(cs.n_reads for (cb, _), cs in stats.items() if cb == NO_BARCODE)
27
+ cb_reads = total_reads - no_cb_reads
28
+
29
+ hdr = Table.grid(padding=(0, 2))
30
+ hdr.add_column(style="bold cyan")
31
+ hdr.add_column()
32
+ hdr.add_row("total mapped reads", f"{total_reads:>14,}")
33
+ hdr.add_row(
34
+ f"reads with {barcode_tag} tag",
35
+ f"{cb_reads:>14,} ({_pct(cb_reads, total_reads)})",
36
+ )
37
+ hdr.add_row(
38
+ f"reads without {barcode_tag} tag",
39
+ f"{no_cb_reads:>14,} ({_pct(no_cb_reads, total_reads)})",
40
+ )
41
+ hdr.add_row(f"unique {_id_label(barcode_tag)}", f"{df['cb'].nunique():>14,}")
42
+ if group_by == "cell-contig":
43
+ hdr.add_row("contigs with reads", f"{df['contig'].nunique():>14,}")
44
+ hdr.add_row("rows (cell × contig)", f"{len(df):>14,}")
45
+ else:
46
+ hdr.add_row("rows (per cell)", f"{len(df):>14,}")
47
+
48
+ console.print(
49
+ Panel(
50
+ hdr,
51
+ title="[bold]BAM QC summary[/bold]",
52
+ border_style="cyan",
53
+ padding=(1, 2),
54
+ )
55
+ )
56
+ if df.empty:
57
+ return
58
+
59
+ def _metric_table(title: str, col: str, digits: int) -> Table:
60
+ t = Table(
61
+ title=title,
62
+ title_style="bold",
63
+ header_style="bold cyan",
64
+ border_style="dim",
65
+ padding=(0, 1),
66
+ )
67
+ t.add_column("metric", style="cyan", no_wrap=True)
68
+ t.add_column("value", justify="right")
69
+ for k, v in df[col].describe().round(digits).items():
70
+ t.add_row(str(k), str(v))
71
+ return t
72
+
73
+ console.print()
74
+ console.print(
75
+ _metric_table("MAPQ (per-cell means)", "mean_mapq", 2),
76
+ _metric_table("Base quality (per-cell means)", "mean_base_qual", 2),
77
+ _metric_table("Clip fraction (per-cell means)", "mean_clip_frac", 4),
78
+ )
79
+ console.print()