pyedger 0.1.2__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyedger
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Pure-Python port of Bioconductor edgeR — negative-binomial models for differential expression of count data (estimateDisp, glmFit, glmLRT, glmQLFTest, exactTest).
5
5
  Author-email: Zehua Zeng <starlitnightly@163.com>
6
6
  License: LGPL-3.0-or-later
@@ -33,7 +33,7 @@ Dynamic: license-file
33
33
  A **pure-Python port of [Bioconductor edgeR](https://bioconductor.org/packages/release/bioc/html/edgeR.html)** (Robinson, McCarthy & Smyth, *Bioinformatics* 2010) — negative-binomial models for differential expression of count data.
34
34
 
35
35
  - **No `rpy2`**, no R install — the edgeR negative-binomial GLM workflow reimplemented in NumPy / SciPy, vectorised over genes
36
- - Matches Bioconductor edgeR 4.0 numerically (see [Numerical parity](#numerical-parity-with-edger))
36
+ - Matches Bioconductor edgeR 4.10 numerically (see [Numerical parity](#numerical-parity-with-edger))
37
37
  - The canonical pipelines: `DGEList → calcNormFactors → estimateDisp → glmQLFit → glmQLFTest` and the classic `exactTest`
38
38
  - TMM normalization, common / trended / tagwise dispersion, GLM and quasi-likelihood F-tests, `filterByExpr`, `cpm` / `aveLogCPM`
39
39
  - Both Python-style (`glm_fit`, `estimate_disp`, `top_tags`) and R-style (`glmFit`, `estimateDisp`, `topTags`) names exported
@@ -93,27 +93,31 @@ pyedger.topTags(et)
93
93
 
94
94
  ## Numerical parity with edgeR
95
95
 
96
- `tests/test_r_parity.py` checks pyedger against edgeR 4.0.16 (R 4.3.3, limma 3.58.1,
97
- locfit 1.5-9) on a paired design with zero-count library pairs; the reference
98
- values are regenerated by `tests/data/make_reference.R`. Since 0.1.1 the GLM
99
- kernels follow edgeR's compiled code step by step and run vectorised over all
100
- genes:
96
+ `tests/test_r_parity.py` checks pyedger against edgeR 4.10.5 (R 4.5.3, limma 3.68.5,
97
+ locfit 1.5-9.12) on a paired design with zero-count library pairs, a Milo-style
98
+ design and limma hyperparameter cases; the reference values are regenerated by
99
+ `tests/data/make_reference.R`. The GLM kernels follow edgeR's compiled code step
100
+ by step and run vectorised over all genes:
101
101
 
102
102
  | Step | Ported from | Agreement with R |
103
103
  |---|---|---|
104
- | `glmFit` (Levenberg, one-way shortcut, start values) | `glm_levenberg.cpp`, `glm_one_group.cpp` | ~1e-9 |
105
- | `estimateDisp` with a design (Cox-Reid APL, zero-fit groups) and classic mode | `R_compute_apl.cpp`, `adj_coxreid.cpp`, `q2qnbinom` | ~1e-10 |
104
+ | `glmFit` (Levenberg, one-way shortcut, start values) | `glm.c` | ~1e-9 |
105
+ | `estimateDisp` with a design (Cox-Reid APL, zero-fit groups) and classic mode | `compute_apl.c`, `q2qnbinom` | ~1e-10 |
106
106
  | Trend: `locfitByCol` | locfit tree evaluation + interpolation | bit-identical |
107
- | `maximizeInterpolant`, `aveLogCPM` | `interpolator.cpp`, `R_ave_log_cpm.cpp` | ~1e-13 |
108
- | `squeezeVar` / `fitFDist` with a covariate | limma (natural-spline trend) | ~1e-14 |
109
- | `glmQLFit(legacy=False)` bias-adjusted deviance/df | `ql_glm.c`, `ql_weights.c`, `.computePriorS2` | ~1e-10 |
110
-
111
- `glmQLFit(legacy=False)` and `estimateGLMCommonDisp` depend on edgeR's
112
- `optimize(tol=1e-5)` over dispersion^(1/4), so R's own value is only defined to
113
- ~4e-5 relative; agreement there is at that level.
114
-
115
- Defaults follow edgeR 4.0: `glmQLFit(legacy=True, abundance_trend=True)` and
116
- `estimateDisp(prior_df=None)` (prior df estimated).
107
+ | `squeezeVar` (`fitFDist`, `fitFDistUnequalDF1`, `fitFDistRobustly`) | limma 3.68 | ~1e-11 |
108
+ | `glmQLFit(legacy=False)` (new QL) given a dispersion | `ql_glm.c` | ~1e-9 |
109
+ | `glmQLFit(legacy=True)`, `exactTest`, `glmTreat` | | ~1e-10 |
110
+
111
+ `glmQLFit(legacy=False)` without a dispersion and `estimateGLMCommonDisp` estimate
112
+ the NB dispersion with edgeR's `optimize(tol=1e-5)` over dispersion^(1/4); R's own
113
+ value is only defined to ~4e-5 relative, and agreement there is at that level
114
+ (p-values ~1e-4 relative on the test data). On real data the agreement is usually
115
+ much closer: on four GSE274113 pseudobulk ATAC comparisons (~170k peaks x 26
116
+ samples, `~ library + condition`, default new QL) p-values agreed to < 1e-7
117
+ relative with identical FDR < 0.1 calls.
118
+
119
+ Defaults follow edgeR 4.10: `glmQLFit(legacy=False, abundance_trend=True)`,
120
+ `estimateDisp(prior_df=None)` with the `chooseLowessSpan` span.
117
121
 
118
122
  ## Citation
119
123
 
@@ -3,7 +3,7 @@
3
3
  A **pure-Python port of [Bioconductor edgeR](https://bioconductor.org/packages/release/bioc/html/edgeR.html)** (Robinson, McCarthy & Smyth, *Bioinformatics* 2010) — negative-binomial models for differential expression of count data.
4
4
 
5
5
  - **No `rpy2`**, no R install — the edgeR negative-binomial GLM workflow reimplemented in NumPy / SciPy, vectorised over genes
6
- - Matches Bioconductor edgeR 4.0 numerically (see [Numerical parity](#numerical-parity-with-edger))
6
+ - Matches Bioconductor edgeR 4.10 numerically (see [Numerical parity](#numerical-parity-with-edger))
7
7
  - The canonical pipelines: `DGEList → calcNormFactors → estimateDisp → glmQLFit → glmQLFTest` and the classic `exactTest`
8
8
  - TMM normalization, common / trended / tagwise dispersion, GLM and quasi-likelihood F-tests, `filterByExpr`, `cpm` / `aveLogCPM`
9
9
  - Both Python-style (`glm_fit`, `estimate_disp`, `top_tags`) and R-style (`glmFit`, `estimateDisp`, `topTags`) names exported
@@ -63,27 +63,31 @@ pyedger.topTags(et)
63
63
 
64
64
  ## Numerical parity with edgeR
65
65
 
66
- `tests/test_r_parity.py` checks pyedger against edgeR 4.0.16 (R 4.3.3, limma 3.58.1,
67
- locfit 1.5-9) on a paired design with zero-count library pairs; the reference
68
- values are regenerated by `tests/data/make_reference.R`. Since 0.1.1 the GLM
69
- kernels follow edgeR's compiled code step by step and run vectorised over all
70
- genes:
66
+ `tests/test_r_parity.py` checks pyedger against edgeR 4.10.5 (R 4.5.3, limma 3.68.5,
67
+ locfit 1.5-9.12) on a paired design with zero-count library pairs, a Milo-style
68
+ design and limma hyperparameter cases; the reference values are regenerated by
69
+ `tests/data/make_reference.R`. The GLM kernels follow edgeR's compiled code step
70
+ by step and run vectorised over all genes:
71
71
 
72
72
  | Step | Ported from | Agreement with R |
73
73
  |---|---|---|
74
- | `glmFit` (Levenberg, one-way shortcut, start values) | `glm_levenberg.cpp`, `glm_one_group.cpp` | ~1e-9 |
75
- | `estimateDisp` with a design (Cox-Reid APL, zero-fit groups) and classic mode | `R_compute_apl.cpp`, `adj_coxreid.cpp`, `q2qnbinom` | ~1e-10 |
74
+ | `glmFit` (Levenberg, one-way shortcut, start values) | `glm.c` | ~1e-9 |
75
+ | `estimateDisp` with a design (Cox-Reid APL, zero-fit groups) and classic mode | `compute_apl.c`, `q2qnbinom` | ~1e-10 |
76
76
  | Trend: `locfitByCol` | locfit tree evaluation + interpolation | bit-identical |
77
- | `maximizeInterpolant`, `aveLogCPM` | `interpolator.cpp`, `R_ave_log_cpm.cpp` | ~1e-13 |
78
- | `squeezeVar` / `fitFDist` with a covariate | limma (natural-spline trend) | ~1e-14 |
79
- | `glmQLFit(legacy=False)` bias-adjusted deviance/df | `ql_glm.c`, `ql_weights.c`, `.computePriorS2` | ~1e-10 |
80
-
81
- `glmQLFit(legacy=False)` and `estimateGLMCommonDisp` depend on edgeR's
82
- `optimize(tol=1e-5)` over dispersion^(1/4), so R's own value is only defined to
83
- ~4e-5 relative; agreement there is at that level.
84
-
85
- Defaults follow edgeR 4.0: `glmQLFit(legacy=True, abundance_trend=True)` and
86
- `estimateDisp(prior_df=None)` (prior df estimated).
77
+ | `squeezeVar` (`fitFDist`, `fitFDistUnequalDF1`, `fitFDistRobustly`) | limma 3.68 | ~1e-11 |
78
+ | `glmQLFit(legacy=False)` (new QL) given a dispersion | `ql_glm.c` | ~1e-9 |
79
+ | `glmQLFit(legacy=True)`, `exactTest`, `glmTreat` | | ~1e-10 |
80
+
81
+ `glmQLFit(legacy=False)` without a dispersion and `estimateGLMCommonDisp` estimate
82
+ the NB dispersion with edgeR's `optimize(tol=1e-5)` over dispersion^(1/4); R's own
83
+ value is only defined to ~4e-5 relative, and agreement there is at that level
84
+ (p-values ~1e-4 relative on the test data). On real data the agreement is usually
85
+ much closer: on four GSE274113 pseudobulk ATAC comparisons (~170k peaks x 26
86
+ samples, `~ library + condition`, default new QL) p-values agreed to < 1e-7
87
+ relative with identical FDR < 0.1 calls.
88
+
89
+ Defaults follow edgeR 4.10: `glmQLFit(legacy=False, abundance_trend=True)`,
90
+ `estimateDisp(prior_df=None)` with the `chooseLowessSpan` span.
87
91
 
88
92
  ## Citation
89
93
 
@@ -31,6 +31,7 @@ from .core import (
31
31
  )
32
32
 
33
33
  calcNormFactors = calc_norm_factors
34
+ normLibSizes = calc_norm_factors # edgeR >= 4.0 name for calcNormFactors
34
35
  effectiveLibSizes = effective_lib_sizes
35
36
  equalizeLibSizes = equalize_lib_sizes
36
37
  estimateCommonDisp = estimate_common_disp
@@ -65,6 +66,7 @@ __all__ = [
65
66
  "aveLogCPM",
66
67
  "calc_norm_factors",
67
68
  "calcNormFactors",
69
+ "normLibSizes",
68
70
  "effective_lib_sizes",
69
71
  "effectiveLibSizes",
70
72
  "equalize_lib_sizes",
@@ -20,7 +20,7 @@ LOG_LOW_VALUE = np.log(LOW_VALUE)
20
20
 
21
21
  # --------------------------------------------------------------------------- deviance
22
22
  def unit_nb_deviance(y, mu, phi):
23
- """nbdev.c compute_unit_nb_deviance, elementwise with broadcasting."""
23
+ """compute_nbdev.c compute_unit_nb_deviance (edgeR 4.10), elementwise with broadcasting."""
24
24
  y = np.asarray(y, dtype=float) + 1e-8
25
25
  mu = np.asarray(mu, dtype=float) + 1e-8
26
26
  y, mu, phi = np.broadcast_arrays(y, mu, np.asarray(phi, dtype=float))
@@ -38,7 +38,7 @@ def unit_nb_deviance(y, mu, phi):
38
38
  nb = rest & ~(prod > 1e6)
39
39
  inv = 1.0 / phi[nb]
40
40
  out[nb] = 2 * (y[nb] * np.log(y[nb] / mu[nb]) + (y[nb] + inv) * np.log((mu[nb] + inv) / (y[nb] + inv)))
41
- return out
41
+ return np.maximum(out, 0.0) # edgeR >= 4.4 floors unit deviances at 0
42
42
 
43
43
 
44
44
  def nb_deviance(Y, Mu, Phi, W):