bootstrap2-sc 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.
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: bootstrap2-sc
3
+ Version: 0.1.0
4
+ Summary: Subject-aware two-level hierarchical bootstrap for single-cell differential expression
5
+ Author: ZW.G
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: anndata<1,>=0.10
9
+ Requires-Dist: joblib<2,>=1.3
10
+ Requires-Dist: numpy<3,>=1.24
11
+ Requires-Dist: pandas<4,>=2
12
+ Requires-Dist: scipy<2,>=1.10
13
+
14
+ # BOOTSTRAP2
15
+
16
+ Two-level hierarchical bootstrap for single-cell differential expression.
17
+
18
+ ## Installation
19
+
20
+ Python 3.10 or newer is required. Install with:
21
+
22
+ ```sh
23
+ python -m pip install bootstrap2-sc
24
+ ```
25
+
26
+ For installation from a local source checkout:
27
+
28
+ ```sh
29
+ python -m pip install .
30
+ ```
31
+
32
+ ## Input
33
+
34
+ Provide an AnnData object containing raw counts in `adata.X`.
35
+
36
+ | `adata.obs` field | Description |
37
+ | --- | --- |
38
+ | `Sample` | Globally unique biological subject identifier |
39
+ | `Condition` | Exactly two comparison groups |
40
+ | `Batch` | Batch identifier; optional for a single-batch design |
41
+
42
+ Each subject must belong to one condition and one batch. The method supports
43
+ the nested batch--subject design, not general covariate adjustment,
44
+ paired/repeated subjects or condition-batch confounding.
45
+
46
+ ## Python API
47
+
48
+ ```python
49
+ import anndata as ad
50
+ from bootstrap2 import bootstrap2h
51
+
52
+ adata = ad.read_h5ad("counts.h5ad")
53
+ result = bootstrap2h(
54
+ adata,
55
+ condition=("SLE", "Control"),
56
+ n_bootstraps=10000,
57
+ n_jobs=4,
58
+ seed=0,
59
+ lfc=0.0,
60
+ )
61
+ result.to_csv("results.tsv", sep="\t")
62
+ ```
63
+
64
+ The input AnnData is not modified. Use `layer="counts"` to select a raw-count
65
+ layer in the API.
66
+
67
+ | Parameter | Default | Description |
68
+ | --- | --- | --- |
69
+ | `n_bootstraps` | 10000 | Number of bootstrap iterations |
70
+ | `n_jobs` | 4 | Worker processes; -1 uses available CPUs |
71
+ | `seed` | 0 | Random seed |
72
+ | `lfc` | 0 | Nonnegative log2 threshold; 0 gives the point null |
73
+ | `condition` | First-appearance order | Numerator then denominator condition |
74
+
75
+ With `condition=("SLE", "Control")`, a positive LFC indicates higher expression
76
+ in SLE. Use `condition_col`, `sample_col` and `batch_col` for alternative
77
+ metadata field names.
78
+
79
+ ## Output
80
+
81
+ Returns a DataFrame indexed by gene, with `p_val` and `lfc` columns.
82
+
@@ -0,0 +1,69 @@
1
+ # BOOTSTRAP2
2
+
3
+ Two-level hierarchical bootstrap for single-cell differential expression.
4
+
5
+ ## Installation
6
+
7
+ Python 3.10 or newer is required. Install with:
8
+
9
+ ```sh
10
+ python -m pip install bootstrap2-sc
11
+ ```
12
+
13
+ For installation from a local source checkout:
14
+
15
+ ```sh
16
+ python -m pip install .
17
+ ```
18
+
19
+ ## Input
20
+
21
+ Provide an AnnData object containing raw counts in `adata.X`.
22
+
23
+ | `adata.obs` field | Description |
24
+ | --- | --- |
25
+ | `Sample` | Globally unique biological subject identifier |
26
+ | `Condition` | Exactly two comparison groups |
27
+ | `Batch` | Batch identifier; optional for a single-batch design |
28
+
29
+ Each subject must belong to one condition and one batch. The method supports
30
+ the nested batch--subject design, not general covariate adjustment,
31
+ paired/repeated subjects or condition-batch confounding.
32
+
33
+ ## Python API
34
+
35
+ ```python
36
+ import anndata as ad
37
+ from bootstrap2 import bootstrap2h
38
+
39
+ adata = ad.read_h5ad("counts.h5ad")
40
+ result = bootstrap2h(
41
+ adata,
42
+ condition=("SLE", "Control"),
43
+ n_bootstraps=10000,
44
+ n_jobs=4,
45
+ seed=0,
46
+ lfc=0.0,
47
+ )
48
+ result.to_csv("results.tsv", sep="\t")
49
+ ```
50
+
51
+ The input AnnData is not modified. Use `layer="counts"` to select a raw-count
52
+ layer in the API.
53
+
54
+ | Parameter | Default | Description |
55
+ | --- | --- | --- |
56
+ | `n_bootstraps` | 10000 | Number of bootstrap iterations |
57
+ | `n_jobs` | 4 | Worker processes; -1 uses available CPUs |
58
+ | `seed` | 0 | Random seed |
59
+ | `lfc` | 0 | Nonnegative log2 threshold; 0 gives the point null |
60
+ | `condition` | First-appearance order | Numerator then denominator condition |
61
+
62
+ With `condition=("SLE", "Control")`, a positive LFC indicates higher expression
63
+ in SLE. Use `condition_col`, `sample_col` and `batch_col` for alternative
64
+ metadata field names.
65
+
66
+ ## Output
67
+
68
+ Returns a DataFrame indexed by gene, with `p_val` and `lfc` columns.
69
+
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bootstrap2-sc"
7
+ version = "0.1.0"
8
+ description = "Subject-aware two-level hierarchical bootstrap for single-cell differential expression"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{name = "ZW.G"}]
12
+ dependencies = [
13
+ "anndata>=0.10,<1",
14
+ "joblib>=1.3,<2",
15
+ "numpy>=1.24,<3",
16
+ "pandas>=2,<4",
17
+ "scipy>=1.10,<2",
18
+ ]
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
22
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """BOOTSTRAP2: hierarchical bootstrap for scRNA-seq DEA."""
2
+
3
+ from .core import bootstrap2h
4
+
5
+ __all__ = ["bootstrap2h", "__version__"]
6
+ __version__ = "0.1.0"
7
+
@@ -0,0 +1,173 @@
1
+ """Two-level batch--subject bootstrap, without cell-level resampling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from numbers import Integral
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ from joblib import Parallel, delayed, effective_n_jobs
11
+ from scipy import sparse
12
+
13
+
14
+ def _matrix(adata, layer):
15
+ if layer is not None and layer not in adata.layers:
16
+ raise ValueError(f"Layer {layer!r} is not present in adata.layers.")
17
+ counts = adata.X if layer is None else adata.layers[layer]
18
+ if counts is None:
19
+ raise ValueError("AnnData must contain a raw count matrix.")
20
+ matrix = sparse.csr_matrix(counts, copy=True)
21
+ if not np.isfinite(matrix.data).all() or (matrix.data < 0).any():
22
+ raise ValueError("Expression must contain finite, nonnegative values.")
23
+ if matrix.dtype.kind != "f":
24
+ matrix = matrix.astype(np.float32)
25
+ totals = np.asarray(matrix.sum(axis=1)).ravel()
26
+ if not np.isfinite(totals).all():
27
+ raise ValueError("Nonfinite cell library sizes; check expression values.")
28
+ zero = totals == 0
29
+ if zero.any():
30
+ warnings.warn(
31
+ f"{zero.sum()} cells have zero total counts and remain zero after CPM normalization.",
32
+ UserWarning, stacklevel=3,
33
+ )
34
+ scale = totals / 1e6
35
+ scale[zero] = 1
36
+ matrix.data /= np.repeat(scale, np.diff(matrix.indptr))
37
+ return matrix
38
+
39
+
40
+ def _prepare(adata, matrix, condition_col, sample_col, batch_col, condition):
41
+ for column in [condition_col, sample_col]:
42
+ if column not in adata.obs:
43
+ raise ValueError(f"Required metadata column {column!r} is missing.")
44
+ columns = [condition_col, sample_col]
45
+ if batch_col is not None and batch_col in adata.obs:
46
+ columns.append(batch_col)
47
+ if len(set(columns)) != len(columns):
48
+ raise ValueError("Condition, sample and batch columns must be different.")
49
+ obs = adata.obs[columns].copy()
50
+ if obs.isna().any().any():
51
+ raise ValueError("Condition, sample and batch labels must not be missing.")
52
+ # Reject labels whose string conversion would merge different identifiers.
53
+ for column in columns:
54
+ if obs[column].nunique() != obs[column].astype(str).nunique():
55
+ raise ValueError(f"Labels in {column!r} collide after conversion to strings.")
56
+ obs[column] = obs[column].astype(str)
57
+ batches = obs[batch_col] if batch_col in obs else pd.Series("single_batch", index=obs.index)
58
+ labels = obs[condition_col].unique().tolist()
59
+ if len(labels) != 2:
60
+ raise ValueError("Exactly two conditions are required; subset the AnnData first.")
61
+ if condition is None:
62
+ condition = tuple(labels)
63
+ else:
64
+ if isinstance(condition, (str, bytes)):
65
+ raise ValueError("condition must be a pair of condition labels, not a string.")
66
+ condition = tuple(str(value) for value in condition)
67
+ if len(condition) != 2 or condition[0] == condition[1] or set(condition) != set(labels):
68
+ raise ValueError("condition must contain the two observed conditions, numerator first.")
69
+ layout = pd.DataFrame({"sample": obs[sample_col], "condition": obs[condition_col], "batch": batches})
70
+ for column in ["condition", "batch"]:
71
+ if (layout.groupby("sample", sort=False)[column].nunique() > 1).any():
72
+ raise ValueError(
73
+ "Each Sample must belong to one condition and one batch. "
74
+ "Repeated/paired subjects or subjects spanning batches require a different design."
75
+ )
76
+ valid, dropped, group_a, group_b = [], [], {}, {}
77
+ for batch in layout["batch"].unique():
78
+ in_batch = layout["batch"] == batch
79
+ a = layout.loc[in_batch & (layout["condition"] == condition[0]), "sample"].unique()
80
+ b = layout.loc[in_batch & (layout["condition"] == condition[1]), "sample"].unique()
81
+ if len(a) and len(b):
82
+ valid.append(batch)
83
+ group_a[batch], group_b[batch] = a, b
84
+ else:
85
+ dropped.append(batch)
86
+ if dropped:
87
+ raise ValueError("Each batch must contain both conditions. Batches lacking one condition: " + ", ".join(dropped))
88
+ if not valid:
89
+ raise ValueError("No batch contains both conditions; condition and batch may be confounded.")
90
+ all_samples = [sample for batch in valid for group in (group_a, group_b) for sample in group[batch]]
91
+ counts = [sum(len(group[batch]) for batch in valid) for group in (group_a, group_b)]
92
+ if min(counts) < 2:
93
+ raise ValueError("At least two independent subjects per condition must remain.")
94
+ positions = {sample: i for i, sample in enumerate(all_samples)}
95
+ transposed = matrix.T.tocsc()
96
+ profiles = np.array([
97
+ np.asarray(transposed[:, np.flatnonzero(layout["sample"].to_numpy() == sample)].mean(axis=1)).ravel()
98
+ for sample in all_samples
99
+ ])
100
+ indices_a = {batch: np.array([positions[s] for s in group_a[batch]]) for batch in valid}
101
+ indices_b = {batch: np.array([positions[s] for s in group_b[batch]]) for batch in valid}
102
+ return profiles, indices_a, indices_b
103
+
104
+
105
+ def _worker(profiles, indices_a, indices_b, fold_threshold, n_iters, seed):
106
+ rng = np.random.RandomState(seed)
107
+ up = np.zeros(profiles.shape[1], dtype=np.int64)
108
+ down = np.zeros(profiles.shape[1], dtype=np.int64)
109
+ batches_a, batches_b = np.array(list(indices_a)), np.array(list(indices_b))
110
+ for _ in range(n_iters):
111
+ sampled_a = rng.choice(batches_a, size=len(batches_a), replace=True)
112
+ sampled_b = rng.choice(batches_b, size=len(batches_b), replace=True)
113
+ a = np.concatenate([rng.choice(indices_a[b], size=len(indices_a[b]), replace=True) for b in sampled_a])
114
+ b = np.concatenate([rng.choice(indices_b[b], size=len(indices_b[b]), replace=True) for b in sampled_b])
115
+ mean_a, mean_b = profiles[a].mean(axis=0), profiles[b].mean(axis=0)
116
+ up += mean_a <= mean_b * fold_threshold
117
+ down += mean_b <= mean_a * fold_threshold
118
+ return up, down
119
+
120
+
121
+ def _adjust_pvalues(up, down, n_iters):
122
+ return (2.0 * np.minimum(up, down) + 1.0) / (n_iters + 1.0)
123
+
124
+
125
+ def bootstrap2h(
126
+ adata, *, condition=None, n_bootstraps=10_000, n_jobs=4, seed=0, lfc=0.0,
127
+ condition_col="Condition", sample_col="Sample", batch_col="Batch", layer=None,
128
+ ):
129
+ """Run two-level bootstrap on raw counts without modifying the input.
130
+
131
+ ``condition`` is the numerator/denominator pair; if omitted, use label order
132
+ in ``adata.obs``. ``lfc`` is a nonnegative log2 threshold (0 for point null).
133
+ Metadata defaults to Condition, Sample and Batch; a missing Batch column
134
+ means one batch. ``layer`` optionally selects a raw-count layer instead of X.
135
+ ``n_jobs`` is the worker count; -1 uses available CPUs.
136
+ Return a gene-indexed DataFrame with unadjusted ``p_val`` and ``lfc``.
137
+ """
138
+ if adata.n_obs == 0 or adata.n_vars == 0:
139
+ raise ValueError("AnnData must contain cells and genes.")
140
+ if not adata.var_names.is_unique:
141
+ raise ValueError("Gene names must be unique.")
142
+ if isinstance(n_bootstraps, bool) or not isinstance(n_bootstraps, Integral) or n_bootstraps < 1:
143
+ raise ValueError("n_bootstraps must be a positive integer.")
144
+ if isinstance(n_jobs, bool) or not isinstance(n_jobs, Integral) or (n_jobs < 1 and n_jobs != -1):
145
+ raise ValueError("n_jobs must be a positive integer or -1.")
146
+ if isinstance(seed, bool) or not isinstance(seed, Integral) or not 0 <= seed < 2**32:
147
+ raise ValueError("seed must be an integer in [0, 2**32).")
148
+ if not np.isfinite(lfc) or lfc < 0:
149
+ raise ValueError("lfc must be finite and nonnegative.")
150
+ with np.errstate(over="ignore"):
151
+ fold_threshold = float(np.exp2(lfc))
152
+ if not np.isfinite(fold_threshold):
153
+ raise ValueError("lfc is too large.")
154
+ matrix = _matrix(adata, layer)
155
+ profiles, a, b = _prepare(
156
+ adata, matrix, condition_col, sample_col, batch_col, condition,
157
+ )
158
+ workers = min(effective_n_jobs(n_jobs), n_bootstraps)
159
+ iterations = [n_bootstraps // workers + (i < n_bootstraps % workers) for i in range(workers)]
160
+ seeds = np.random.RandomState(seed).randint(0, 1_000_000, size=workers)
161
+ results = Parallel(n_jobs=workers)(
162
+ delayed(_worker)(profiles, a, b, fold_threshold, count, int(worker_seed))
163
+ for count, worker_seed in zip(iterations, seeds)
164
+ )
165
+ up, down = np.sum([r[0] for r in results], axis=0), np.sum([r[1] for r in results], axis=0)
166
+ mean_a = profiles[np.concatenate(list(a.values()))].mean(axis=0)
167
+ mean_b = profiles[np.concatenate(list(b.values()))].mean(axis=0)
168
+ result = pd.DataFrame({
169
+ "p_val": _adjust_pvalues(up, down, n_bootstraps),
170
+ "lfc": np.log2((mean_a + 1e-9) / (mean_b + 1e-9)),
171
+ }, index=pd.Index(adata.var_names, name="gene"))
172
+ return result
173
+
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: bootstrap2-sc
3
+ Version: 0.1.0
4
+ Summary: Subject-aware two-level hierarchical bootstrap for single-cell differential expression
5
+ Author: ZW.G
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: anndata<1,>=0.10
9
+ Requires-Dist: joblib<2,>=1.3
10
+ Requires-Dist: numpy<3,>=1.24
11
+ Requires-Dist: pandas<4,>=2
12
+ Requires-Dist: scipy<2,>=1.10
13
+
14
+ # BOOTSTRAP2
15
+
16
+ Two-level hierarchical bootstrap for single-cell differential expression.
17
+
18
+ ## Installation
19
+
20
+ Python 3.10 or newer is required. Install with:
21
+
22
+ ```sh
23
+ python -m pip install bootstrap2-sc
24
+ ```
25
+
26
+ For installation from a local source checkout:
27
+
28
+ ```sh
29
+ python -m pip install .
30
+ ```
31
+
32
+ ## Input
33
+
34
+ Provide an AnnData object containing raw counts in `adata.X`.
35
+
36
+ | `adata.obs` field | Description |
37
+ | --- | --- |
38
+ | `Sample` | Globally unique biological subject identifier |
39
+ | `Condition` | Exactly two comparison groups |
40
+ | `Batch` | Batch identifier; optional for a single-batch design |
41
+
42
+ Each subject must belong to one condition and one batch. The method supports
43
+ the nested batch--subject design, not general covariate adjustment,
44
+ paired/repeated subjects or condition-batch confounding.
45
+
46
+ ## Python API
47
+
48
+ ```python
49
+ import anndata as ad
50
+ from bootstrap2 import bootstrap2h
51
+
52
+ adata = ad.read_h5ad("counts.h5ad")
53
+ result = bootstrap2h(
54
+ adata,
55
+ condition=("SLE", "Control"),
56
+ n_bootstraps=10000,
57
+ n_jobs=4,
58
+ seed=0,
59
+ lfc=0.0,
60
+ )
61
+ result.to_csv("results.tsv", sep="\t")
62
+ ```
63
+
64
+ The input AnnData is not modified. Use `layer="counts"` to select a raw-count
65
+ layer in the API.
66
+
67
+ | Parameter | Default | Description |
68
+ | --- | --- | --- |
69
+ | `n_bootstraps` | 10000 | Number of bootstrap iterations |
70
+ | `n_jobs` | 4 | Worker processes; -1 uses available CPUs |
71
+ | `seed` | 0 | Random seed |
72
+ | `lfc` | 0 | Nonnegative log2 threshold; 0 gives the point null |
73
+ | `condition` | First-appearance order | Numerator then denominator condition |
74
+
75
+ With `condition=("SLE", "Control")`, a positive LFC indicates higher expression
76
+ in SLE. Use `condition_col`, `sample_col` and `batch_col` for alternative
77
+ metadata field names.
78
+
79
+ ## Output
80
+
81
+ Returns a DataFrame indexed by gene, with `p_val` and `lfc` columns.
82
+
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/bootstrap2/__init__.py
4
+ src/bootstrap2/core.py
5
+ src/bootstrap2_sc.egg-info/PKG-INFO
6
+ src/bootstrap2_sc.egg-info/SOURCES.txt
7
+ src/bootstrap2_sc.egg-info/dependency_links.txt
8
+ src/bootstrap2_sc.egg-info/requires.txt
9
+ src/bootstrap2_sc.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ anndata<1,>=0.10
2
+ joblib<2,>=1.3
3
+ numpy<3,>=1.24
4
+ pandas<4,>=2
5
+ scipy<2,>=1.10