pyedger 0.2.1__tar.gz → 0.2.2__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.
- {pyedger-0.2.1 → pyedger-0.2.2}/PKG-INFO +1 -1
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger/__init__.py +8 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger/core.py +72 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger.egg-info/PKG-INFO +1 -1
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger.egg-info/SOURCES.txt +1 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/pyproject.toml +1 -1
- pyedger-0.2.2/tests/test_robust_disp.py +47 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/LICENSE +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/README.md +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger/_glm_batch.py +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger/_limma.py +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger/_locfit.py +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger/_numba_kernels.py +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger/_ql_adjust.py +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger.egg-info/dependency_links.txt +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger.egg-info/requires.txt +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/pyedger.egg-info/top_level.txt +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/setup.cfg +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/tests/test_numba_ql.py +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/tests/test_r_parity.py +0 -0
- {pyedger-0.2.1 → pyedger-0.2.2}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyedger
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -16,12 +16,14 @@ from .core import (
|
|
|
16
16
|
estimate_common_disp,
|
|
17
17
|
estimate_disp,
|
|
18
18
|
estimate_glm_common_disp,
|
|
19
|
+
estimate_glm_robust_disp,
|
|
19
20
|
estimate_glm_tagwise_disp,
|
|
20
21
|
estimate_glm_trended_disp,
|
|
21
22
|
estimate_tagwise_disp,
|
|
22
23
|
exact_test,
|
|
23
24
|
filter_by_expr,
|
|
24
25
|
glm_fit,
|
|
26
|
+
glm_residuals,
|
|
25
27
|
glm_lrt,
|
|
26
28
|
glm_ql_fit,
|
|
27
29
|
glm_qlf_test,
|
|
@@ -38,6 +40,8 @@ estimateCommonDisp = estimate_common_disp
|
|
|
38
40
|
estimateTagwiseDisp = estimate_tagwise_disp
|
|
39
41
|
estimateDisp = estimate_disp
|
|
40
42
|
estimateGLMCommonDisp = estimate_glm_common_disp
|
|
43
|
+
estimateGLMRobustDisp = estimate_glm_robust_disp
|
|
44
|
+
glmResiduals = glm_residuals
|
|
41
45
|
estimateGLMTagwiseDisp = estimate_glm_tagwise_disp
|
|
42
46
|
estimateGLMTrendedDisp = estimate_glm_trended_disp
|
|
43
47
|
exactTest = exact_test
|
|
@@ -55,6 +59,10 @@ topTags = top_tags
|
|
|
55
59
|
aveLogCPM = ave_log_cpm
|
|
56
60
|
|
|
57
61
|
__all__ = [
|
|
62
|
+
"estimate_glm_robust_disp",
|
|
63
|
+
"estimateGLMRobustDisp",
|
|
64
|
+
"glm_residuals",
|
|
65
|
+
"glmResiduals",
|
|
58
66
|
"DGEExact",
|
|
59
67
|
"DGEGLM",
|
|
60
68
|
"DGELRT",
|
|
@@ -996,6 +996,78 @@ def _fit_glm_batch(counts, X, offset_mat, phi_mat, W, start=None):
|
|
|
996
996
|
return beta, mu, dev
|
|
997
997
|
|
|
998
998
|
|
|
999
|
+
def glm_residuals(glmfit: DGEGLM, type: str = "pearson") -> np.ndarray:
|
|
1000
|
+
"""Observation residuals of a fitted NB GLM (``pearson`` / ``deviance`` / ``anscombe``)."""
|
|
1001
|
+
y = glmfit.counts.to_numpy(dtype=float)
|
|
1002
|
+
mu = np.asarray(glmfit.fitted_values, dtype=float)
|
|
1003
|
+
disp = np.asarray(glmfit.dispersion, dtype=float)
|
|
1004
|
+
phi = np.broadcast_to(disp.reshape(-1, 1) if disp.ndim else disp, y.shape)
|
|
1005
|
+
type = type.lower()
|
|
1006
|
+
if type == "pearson":
|
|
1007
|
+
return (y - mu) / np.sqrt(np.maximum(mu + phi * mu * mu, 1e-300))
|
|
1008
|
+
if type == "deviance":
|
|
1009
|
+
unit = _nbinom_unit_deviance(y, mu, phi)
|
|
1010
|
+
return np.sign(y - mu) * np.sqrt(unit)
|
|
1011
|
+
if type == "anscombe":
|
|
1012
|
+
# Anscombe residual for the NB family (Hilbe 2011), integral evaluated numerically.
|
|
1013
|
+
def a(t, ph):
|
|
1014
|
+
grid = np.linspace(0.0, 1.0, 65)[None, :]
|
|
1015
|
+
s_ = np.maximum(t[:, None] * grid, 1e-12)
|
|
1016
|
+
f = (s_ + ph[:, None] * s_ * s_) ** (-1.0 / 3.0)
|
|
1017
|
+
trap = getattr(np, "trapezoid", None) or np.trapz
|
|
1018
|
+
return trap(f, grid[0], axis=1) * t
|
|
1019
|
+
yf, mf, pf = y.ravel(), mu.ravel(), phi.ravel()
|
|
1020
|
+
res = (a(yf, pf) - a(mf, pf)) / ((mf + pf * mf * mf) ** (1.0 / 6.0) + 1e-300)
|
|
1021
|
+
return res.reshape(y.shape)
|
|
1022
|
+
raise ValueError("type must be 'pearson', 'deviance' or 'anscombe'.")
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def estimate_glm_robust_disp(
|
|
1026
|
+
y: DGEList,
|
|
1027
|
+
design=None,
|
|
1028
|
+
prior_df: float = 10.0,
|
|
1029
|
+
update_trend: bool = True,
|
|
1030
|
+
trend_method: str = "bin.loess",
|
|
1031
|
+
maxit: int = 6,
|
|
1032
|
+
k: float = 1.345,
|
|
1033
|
+
residual_type: str = "pearson",
|
|
1034
|
+
record: bool = False,
|
|
1035
|
+
) -> DGEList:
|
|
1036
|
+
"""Observation-weighted robust dispersion, mirroring ``edgeR::estimateGLMRobustDisp``.
|
|
1037
|
+
|
|
1038
|
+
Alternates GLM fits with Huber observation weights ``min(1, k / |r|)`` on
|
|
1039
|
+
the fitted residuals ``r`` and re-estimates the trended and tagwise
|
|
1040
|
+
dispersions with those weights (Zhou, Lindsay & Robinson 2014, NAR). The
|
|
1041
|
+
final weights are kept in ``y.weights`` so a following :func:`glm_fit` /
|
|
1042
|
+
:func:`glm_lrt` down-weights isolated aberrant observations.
|
|
1043
|
+
"""
|
|
1044
|
+
if not isinstance(y, DGEList):
|
|
1045
|
+
y = DGEList(y)
|
|
1046
|
+
if design is None:
|
|
1047
|
+
design = _default_design_from_group(y)
|
|
1048
|
+
y.weights = pd.DataFrame(np.ones(y.counts.shape), index=y.counts.index, columns=y.counts.columns)
|
|
1049
|
+
if y.trended_dispersion is None:
|
|
1050
|
+
estimate_glm_trended_disp(y, design=design, method=trend_method)
|
|
1051
|
+
if y.tagwise_dispersion is None:
|
|
1052
|
+
estimate_glm_tagwise_disp(y, design=design, prior_df=prior_df)
|
|
1053
|
+
history = []
|
|
1054
|
+
for _ in range(int(maxit)):
|
|
1055
|
+
fit = glm_fit(y, design=design, dispersion=y.tagwise_dispersion, prior_count=0)
|
|
1056
|
+
res = glm_residuals(fit, type=residual_type)
|
|
1057
|
+
res = np.where(np.isfinite(res), res, 0.0)
|
|
1058
|
+
ares = np.abs(res)
|
|
1059
|
+
wts = np.where(ares <= k, 1.0, k / np.maximum(ares, 1e-300))
|
|
1060
|
+
y.weights = pd.DataFrame(wts, index=y.counts.index, columns=y.counts.columns)
|
|
1061
|
+
if update_trend:
|
|
1062
|
+
estimate_glm_trended_disp(y, design=design, method=trend_method)
|
|
1063
|
+
estimate_glm_tagwise_disp(y, design=design, prior_df=prior_df)
|
|
1064
|
+
if record:
|
|
1065
|
+
history.append({"weights": wts.copy(), "tagwise_dispersion": np.asarray(y.tagwise_dispersion).copy()})
|
|
1066
|
+
if record:
|
|
1067
|
+
y.record = history
|
|
1068
|
+
return y
|
|
1069
|
+
|
|
1070
|
+
|
|
999
1071
|
def pred_fc(y, design, prior_count: float = 0.125, offset=None, dispersion=None, weights=None) -> pd.DataFrame:
|
|
1000
1072
|
"""edgeR-style prior-count shrunken GLM coefficients on log2 scale."""
|
|
1001
1073
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyedger
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyedger"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Pure-Python port of Bioconductor edgeR — negative-binomial models for differential expression of count data (estimateDisp, glmFit, glmLRT, glmQLFTest, exactTest)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""estimate_glm_robust_disp / glm_residuals (edgeR estimateGLMRobustDisp)."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
import pyedger as pe
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _data(seed=1, G=400, n=8, n_out=20):
|
|
9
|
+
rs = np.random.RandomState(seed)
|
|
10
|
+
mu = rs.lognormal(4, 1.2, G)[:, None] * np.ones((1, n))
|
|
11
|
+
disp = 0.1
|
|
12
|
+
Y = rs.negative_binomial(1 / disp, 1 / (1 + disp * mu)).astype(float)
|
|
13
|
+
out = rs.choice(G, n_out, replace=False)
|
|
14
|
+
col = rs.randint(0, n, n_out)
|
|
15
|
+
Y[out, col] *= 20
|
|
16
|
+
counts = pd.DataFrame(Y, index=[f"g{i}" for i in range(G)], columns=[f"s{j}" for j in range(n)])
|
|
17
|
+
design = pd.DataFrame({"Intercept": 1.0, "cond": [0] * (n // 2) + [1] * (n - n // 2)},
|
|
18
|
+
index=counts.columns)
|
|
19
|
+
return counts, design, out, col
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_glm_residuals_pearson_and_deviance():
|
|
23
|
+
counts, design, _, _ = _data()
|
|
24
|
+
y = pe.DGEList(counts, group=["a"] * 4 + ["b"] * 4)
|
|
25
|
+
pe.estimate_disp(y, design=design)
|
|
26
|
+
fit = pe.glm_fit(y, design=design)
|
|
27
|
+
r = pe.glm_residuals(fit, type="pearson")
|
|
28
|
+
mu = fit.fitted_values
|
|
29
|
+
phi = np.asarray(fit.dispersion).reshape(-1, 1)
|
|
30
|
+
np.testing.assert_allclose(r, (counts.to_numpy() - mu) / np.sqrt(mu + phi * mu * mu), rtol=1e-12)
|
|
31
|
+
d = pe.glm_residuals(fit, type="deviance")
|
|
32
|
+
assert d.shape == r.shape and np.all(np.sign(d) == np.sign(counts.to_numpy() - mu))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_robust_disp_downweights_outliers():
|
|
36
|
+
counts, design, out, col = _data()
|
|
37
|
+
y = pe.DGEList(counts, group=["a"] * 4 + ["b"] * 4)
|
|
38
|
+
y = pe.estimateGLMRobustDisp(y, design=design)
|
|
39
|
+
w = y.weights.to_numpy()
|
|
40
|
+
assert np.all(w[out, col] < 0.5)
|
|
41
|
+
mask = np.ones_like(w, dtype=bool)
|
|
42
|
+
mask[out, col] = False
|
|
43
|
+
assert np.median(w[mask]) == 1.0
|
|
44
|
+
assert 0.05 < np.median(y.tagwise_dispersion) < 0.15
|
|
45
|
+
# the weights flow into the GLM fit
|
|
46
|
+
fit = pe.glm_fit(y, design=design)
|
|
47
|
+
assert np.all(np.isfinite(fit.coefficients))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|