pyfauxseq 0.1.0__py3-none-any.whl
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.
- pyfauxseq/__init__.py +13 -0
- pyfauxseq/data/Mm_liver_LD_NC.csv.gz +0 -0
- pyfauxseq/data/Mm_multitissue_LD_ALF.csv.gz +0 -0
- pyfauxseq/data/Mm_multitissue_LD_ALF.json.gz +0 -0
- pyfauxseq/data/__init__.py +1 -0
- pyfauxseq/estimators.py +93 -0
- pyfauxseq/generate_rnaseq.py +415 -0
- pyfauxseq/utils.py +161 -0
- pyfauxseq-0.1.0.dist-info/METADATA +67 -0
- pyfauxseq-0.1.0.dist-info/RECORD +12 -0
- pyfauxseq-0.1.0.dist-info/WHEEL +4 -0
- pyfauxseq-0.1.0.dist-info/licenses/LICENSE +21 -0
pyfauxseq/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Top-level package for Python Implementation of the R Package fauxseq."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from .generate_rnaseq import generate_diffrhythmic_rnaseq, generate_rhythmic_rnaseq
|
|
6
|
+
|
|
7
|
+
__version__ = version("pyfauxseq")
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"generate_diffrhythmic_rnaseq",
|
|
11
|
+
"generate_rhythmic_rnaseq",
|
|
12
|
+
"normalize_counts",
|
|
13
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Empirically estimated mean-dispersion data to sample from."""
|
pyfauxseq/estimators.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Functions for estimating empirical mean and dispersion of genes.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- nb_fit: to fit negative binomial model to count data
|
|
5
|
+
- estimate_disp_dist: to estimate dispersion of genes from multiple samples
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
10
|
+
from concurrent.futures._base import Future
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from numpy.typing import NDArray
|
|
15
|
+
from pandas.core.series import Series
|
|
16
|
+
from scipy.optimize import minimize
|
|
17
|
+
from scipy.stats import nbinom
|
|
18
|
+
|
|
19
|
+
from .utils import downsample
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def nb_fit(x: NDArray) -> pd.Series:
|
|
23
|
+
"""Fit negative binomial parameters to count data.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
x : ndarray
|
|
28
|
+
count data
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
pandas Series
|
|
33
|
+
mean (mu) and reciprocal of dispersion (size) of count data
|
|
34
|
+
"""
|
|
35
|
+
if np.var(x) < np.mean(x):
|
|
36
|
+
return pd.Series({"size": 1e6, "mu": np.mean(x)})
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
m: float = np.mean(x)
|
|
40
|
+
v: float = np.var(x)
|
|
41
|
+
|
|
42
|
+
def neg_log_likelihood(params):
|
|
43
|
+
size, mu = params
|
|
44
|
+
return -np.sum(nbinom.logpmf(x, n=size, p=size / (size + mu)))
|
|
45
|
+
|
|
46
|
+
initial_guess: list[float] = [m / (v - m), m]
|
|
47
|
+
bounds: list[tuple[int, int | float]] = [(0, 1e6), (0, 1e6)]
|
|
48
|
+
result = minimize(
|
|
49
|
+
neg_log_likelihood, initial_guess, bounds=bounds, method="L-BFGS-B"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if result.success:
|
|
53
|
+
return pd.Series({"size": result.x[0], "mu": m})
|
|
54
|
+
else:
|
|
55
|
+
return pd.Series({"size": np.nan, "mu": np.nan})
|
|
56
|
+
except Exception:
|
|
57
|
+
return pd.Series({"size": np.nan, "mu": np.nan})
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def estimate_disp_dist(
|
|
61
|
+
counts: NDArray, parallel: bool = True, ncores: int | None = None
|
|
62
|
+
) -> pd.DataFrame:
|
|
63
|
+
"""Estimate mean and dispersion for all samples in data in parallel.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
counts : ndarray
|
|
68
|
+
count data matrix
|
|
69
|
+
parallel : bool, optional
|
|
70
|
+
should the samples be processed in parallel, by default True
|
|
71
|
+
ncores : _type_, optional
|
|
72
|
+
with how many cores, by default None
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
pandas DataFrame
|
|
77
|
+
estimates of mean and dispersion for all genes in data
|
|
78
|
+
"""
|
|
79
|
+
counts: NDArray = downsample(counts, parallel=parallel, ncores=ncores)
|
|
80
|
+
|
|
81
|
+
if parallel:
|
|
82
|
+
if ncores is None:
|
|
83
|
+
ncores: int = min(32, (os.cpu_count() or 1) + 4)
|
|
84
|
+
with ThreadPoolExecutor(max_workers=ncores) as executor:
|
|
85
|
+
futures: list[Future[Series]] = [
|
|
86
|
+
executor.submit(nb_fit, counts[i, :]) for i in range(counts.shape[0])
|
|
87
|
+
]
|
|
88
|
+
ests: list[Series] = [future.result() for future in as_completed(futures)]
|
|
89
|
+
else:
|
|
90
|
+
ests: list[Series] = [nb_fit(counts[i, :]) for i in range(counts.shape[0])]
|
|
91
|
+
|
|
92
|
+
final_ests: pd.DataFrame = pd.DataFrame(ests).dropna()
|
|
93
|
+
return final_ests
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Functions for generating rhythmic transcriptomic data.
|
|
2
|
+
|
|
3
|
+
This module currently provides:
|
|
4
|
+
- generate_rhythmic_rnaseq: to generate data under one condition
|
|
5
|
+
- generate_diffrhythmic_rnaseq: to generate data under two conditions
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from numpy.random._generator import Generator
|
|
11
|
+
from numpy.typing import ArrayLike, NDArray
|
|
12
|
+
from scipy.stats import nbinom
|
|
13
|
+
|
|
14
|
+
from .utils import load_dataset
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_rhythmic_rnaseq(
|
|
18
|
+
t: ArrayLike = (0, 4, 8, 12, 16, 20),
|
|
19
|
+
reps: int = 1,
|
|
20
|
+
period: int = 24,
|
|
21
|
+
n_genes: int = 10000,
|
|
22
|
+
rhy_frac: float = 0.1,
|
|
23
|
+
min_A_effect: float = 0.2,
|
|
24
|
+
A_spread: float = 0.5,
|
|
25
|
+
emp_dist: dict | str = "liver",
|
|
26
|
+
depth: int = 40000000,
|
|
27
|
+
lib_size_var: tuple[float, float] = (0.8, 1.2),
|
|
28
|
+
seed=None,
|
|
29
|
+
) -> dict[str, pd.DataFrame]:
|
|
30
|
+
"""Generate synthetic rhythmic RNA-seq count data in one condition.
|
|
31
|
+
|
|
32
|
+
This function generates artificial timeseries transcriptomic data under one
|
|
33
|
+
condition, with rhythmically expressed genes and empirically estimated
|
|
34
|
+
variability between replicates/samples.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
t : numpy array-like, optional
|
|
39
|
+
time points at which samples are generated, by default
|
|
40
|
+
(0, 4, 8, 12, 16, 20)
|
|
41
|
+
reps : int or array-like, optional
|
|
42
|
+
number of replicates at each time point, by default 1
|
|
43
|
+
period : int, optional
|
|
44
|
+
period of the rhythmic genes, by default 24
|
|
45
|
+
n_genes : int, optional
|
|
46
|
+
number of genes in the dataset, by default 10000
|
|
47
|
+
rhy_frac : float, optional
|
|
48
|
+
fraction of genes that are rhythmic, by default 0.1
|
|
49
|
+
min_A_effect : float, optional
|
|
50
|
+
minimum amplitude (in log2 fold) of rhythmic genes, by default 0.2
|
|
51
|
+
A_spread : int, optional
|
|
52
|
+
mean of the exponential distribution of rhythmic gene amplitudes (in
|
|
53
|
+
log2 fold), by default 0.5
|
|
54
|
+
emp_dist : dictionary with keys 'mu' and 'size'| str, optional
|
|
55
|
+
empirical mean (mu) and size (1/dispersion) values for a corpus of
|
|
56
|
+
genes or name of a file containing the dictionary, by default "liver"
|
|
57
|
+
(read from mouse liver dataset)
|
|
58
|
+
depth : int, optional
|
|
59
|
+
average sequencing depth of different samples, by default 4e7
|
|
60
|
+
lib_size_var : tuple of floats, optional
|
|
61
|
+
window of variability of sequencing depth of samples about 'depth',
|
|
62
|
+
by default (0.8, 1.2)
|
|
63
|
+
seed : int, optional
|
|
64
|
+
seed to ensure reproducible datasets, by default None
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
dictionary
|
|
69
|
+
counts: pandas DataFrame (n_genes * n_samples) with count data
|
|
70
|
+
params: pandas DataFrame (n_rhythmic_genes * 3) with identity of
|
|
71
|
+
rhythmic genes, and their amplitudes (A) and phases (phi).
|
|
72
|
+
exp_design: pandas DataFrame (n_samples * 1) with time labels of
|
|
73
|
+
individual samples
|
|
74
|
+
|
|
75
|
+
Raises
|
|
76
|
+
------
|
|
77
|
+
ValueError
|
|
78
|
+
if 'reps' is not an int or has different length than 't'
|
|
79
|
+
FileNotFoundError
|
|
80
|
+
if file containing 'emp_dist' does not exist.
|
|
81
|
+
ValueError
|
|
82
|
+
if provided 'emp_dist' is invalid
|
|
83
|
+
|
|
84
|
+
Notes
|
|
85
|
+
-----
|
|
86
|
+
We extended generative of model of gene expression of Soneson & Delorenz
|
|
87
|
+
[SD]_ to also include rhythmic genes.
|
|
88
|
+
|
|
89
|
+
References
|
|
90
|
+
----------
|
|
91
|
+
.. [SD] Soneson C, Delorenzi M. A comparison of methods for differential
|
|
92
|
+
expression analysis of RNA-seq data. BMC Bioinformatics. 2013;14: 91.
|
|
93
|
+
"""
|
|
94
|
+
rng: Generator = np.random.default_rng(seed)
|
|
95
|
+
|
|
96
|
+
if not isinstance(reps, int) and (
|
|
97
|
+
not isinstance(reps, list) or len(reps) != len(t)
|
|
98
|
+
):
|
|
99
|
+
raise ValueError("Length of reps must be 1 or the same as length of t")
|
|
100
|
+
|
|
101
|
+
if isinstance(emp_dist, str):
|
|
102
|
+
match emp_dist:
|
|
103
|
+
case "liver":
|
|
104
|
+
emp_dist: pd.DataFrame = load_dataset("Mm_liver_LD_NC.csv.gz")
|
|
105
|
+
case "multitissue":
|
|
106
|
+
emp_dist: pd.DataFrame = load_dataset("Mm_multitissue_LD_ALF.csv.gz")
|
|
107
|
+
case _:
|
|
108
|
+
emp_dist: pd.DataFrame = load_dataset("Mm_liver_LD_NC.csv.gz")
|
|
109
|
+
elif not isinstance(emp_dist, pd.DataFrame):
|
|
110
|
+
raise ValueError("The provided emp_dist is invalid.")
|
|
111
|
+
|
|
112
|
+
if emp_dist.shape[1] > 2:
|
|
113
|
+
group_cols = list(pd.Index.difference(emp_dist.columns, ["size", "mu"]))
|
|
114
|
+
|
|
115
|
+
unique_groups = emp_dist[group_cols].drop_duplicates()
|
|
116
|
+
sampled_row = unique_groups.iloc[rng.choice(len(unique_groups))]
|
|
117
|
+
|
|
118
|
+
selected_key = (
|
|
119
|
+
sampled_row.iloc[0] if len(group_cols) == 1 else tuple(sampled_row)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
by_param = group_cols[0] if len(group_cols) == 1 else group_cols
|
|
123
|
+
emp_dist = emp_dist.groupby(by=by_param).get_group(selected_key)
|
|
124
|
+
|
|
125
|
+
t: NDArray = np.array(t)
|
|
126
|
+
t: NDArray = np.repeat(t, reps)
|
|
127
|
+
N: int = len(t)
|
|
128
|
+
|
|
129
|
+
G_rhy: NDArray = rng.uniform(size=n_genes) <= rhy_frac
|
|
130
|
+
|
|
131
|
+
A: NDArray = min_A_effect + rng.exponential(A_spread, n_genes)
|
|
132
|
+
|
|
133
|
+
A[~G_rhy] = 0.0
|
|
134
|
+
phi: NDArray = rng.uniform(size=n_genes) * 2 * np.pi * G_rhy
|
|
135
|
+
params: pd.DataFrame = pd.DataFrame(
|
|
136
|
+
{
|
|
137
|
+
"id": [f"g{i + 1}" for i in np.where(G_rhy)[0]],
|
|
138
|
+
"A": A[G_rhy],
|
|
139
|
+
"phi": phi[G_rhy],
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
t_pattern: NDArray = np.cos(phi)[:, None] @ np.cos(
|
|
144
|
+
2 * np.pi * t[None, :] / period
|
|
145
|
+
) + np.sin(phi)[:, None] @ np.sin(2 * np.pi * t[None, :] / period)
|
|
146
|
+
|
|
147
|
+
lib_size_fct: NDArray = rng.uniform(lib_size_var[0], lib_size_var[1], N)
|
|
148
|
+
lib_size: NDArray = lib_size_fct * depth
|
|
149
|
+
|
|
150
|
+
draw: NDArray = rng.choice(emp_dist.shape[0], n_genes, replace=True)
|
|
151
|
+
lambda_: NDArray = emp_dist["mu"].values[draw].reshape(-1, 1)
|
|
152
|
+
|
|
153
|
+
size: NDArray = emp_dist["size"].values[draw].reshape(-1, 1)
|
|
154
|
+
|
|
155
|
+
lambda_: NDArray = np.repeat(lambda_, N, axis=1) * 2 ** (A[:, None] * t_pattern)
|
|
156
|
+
|
|
157
|
+
mu: NDArray = lambda_ / (np.sum(lambda_, axis=0) / lib_size)
|
|
158
|
+
|
|
159
|
+
counts: NDArray[np.int_] = nbinom.rvs(
|
|
160
|
+
n=size, p=size / (mu + size), random_state=seed
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
exp_design = pd.DataFrame(
|
|
164
|
+
{
|
|
165
|
+
"time": t,
|
|
166
|
+
},
|
|
167
|
+
index=pd.Index(
|
|
168
|
+
"".join(rng.choice(list("abcdefghijklmnopqrstuvwxyz"), 5)) for _ in range(N)
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"counts": pd.DataFrame(
|
|
174
|
+
counts,
|
|
175
|
+
index=pd.Index([f"g{i + 1}" for i in range(n_genes)]),
|
|
176
|
+
columns=exp_design.index,
|
|
177
|
+
),
|
|
178
|
+
"params": params,
|
|
179
|
+
"exp_design": exp_design,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def generate_diffrhythmic_rnaseq(
|
|
184
|
+
t: ArrayLike = (0, 4, 8, 12, 16, 20),
|
|
185
|
+
reps: int = 1,
|
|
186
|
+
period: int = 24,
|
|
187
|
+
n_genes: int = 10000,
|
|
188
|
+
rhy_frac: float = 0.1,
|
|
189
|
+
DR_probs: ArrayLike = (1.25, 1.25, 1.25, 1.25),
|
|
190
|
+
min_A_effect: float = 0.2,
|
|
191
|
+
A_spread: float = 0.5,
|
|
192
|
+
DE_frac: float = 0.1,
|
|
193
|
+
min_DE_effect: float = 0.5,
|
|
194
|
+
DE_spread: float = 0.5,
|
|
195
|
+
groups: tuple[str, str] = ("ctrl", "expt"),
|
|
196
|
+
emp_dist: pd.DataFrame | str = "liver",
|
|
197
|
+
depth: int = 40000000,
|
|
198
|
+
lib_size_var: tuple[float, float] = (0.8, 1.2),
|
|
199
|
+
seed=None,
|
|
200
|
+
):
|
|
201
|
+
"""Generate synthetic rhythmic RNA-seq count data in two conditions.
|
|
202
|
+
|
|
203
|
+
This function generates artificial timeseries transcriptomic data under two
|
|
204
|
+
conditions, which includes both differentially rhythmic and differential
|
|
205
|
+
expressed genes, as well as empirically estimated variability between
|
|
206
|
+
replicates/samples.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
t : numpy array, optional
|
|
211
|
+
time points at which samples are generated, by default
|
|
212
|
+
np.arange(0, 21, 4)
|
|
213
|
+
reps : int or array-like, optional
|
|
214
|
+
number of replicates at each time point, by default 1
|
|
215
|
+
period : int, optional
|
|
216
|
+
period of the rhythmic genes, by default 24
|
|
217
|
+
n_genes : int, optional
|
|
218
|
+
number of genes in the dataset, by default 10000
|
|
219
|
+
rhy_frac : float, optional
|
|
220
|
+
fraction of genes that are rhythmic, by default 0.1
|
|
221
|
+
DR_probs : numpy array, optional
|
|
222
|
+
determines the relative average number of ("same", "gain", "loss",
|
|
223
|
+
"change") group elements. The larger the numbers, more concentrated are
|
|
224
|
+
the actual numbers around the averages, by default (1.25, 1.25, 1.25,
|
|
225
|
+
1.25)
|
|
226
|
+
min_A_effect : float, optional
|
|
227
|
+
minimum amplitude (in log2 fold) of rhythmic genes, by default 0.2
|
|
228
|
+
A_spread : float, optional
|
|
229
|
+
mean of the exponential distribution of rhythmic gene amplitudes (in
|
|
230
|
+
log2 fold), by default 0.5
|
|
231
|
+
DE_frac : float, optional
|
|
232
|
+
fraction of differentially expressed (DE) genes, by default 0.1
|
|
233
|
+
min_DE_effect : float, optional
|
|
234
|
+
minimum log2 fold change in expression of DE genes, by default 0.5
|
|
235
|
+
DE_spread : float, optional
|
|
236
|
+
mean of the exponential distribution of DE fold changes, by default 0.5
|
|
237
|
+
groups : tuple, optional
|
|
238
|
+
labels for the two groups/conditions, by default ("ctrl", "expt")
|
|
239
|
+
emp_dist : dictionary with keys 'mu' and 'size'| str, optional
|
|
240
|
+
empirical mean (mu) and size (1/dispersion) values for a corpus of
|
|
241
|
+
genes or name of a file containing the dictionary, by default "liver"
|
|
242
|
+
(read from mouse liver dataset)
|
|
243
|
+
depth : int, optional
|
|
244
|
+
average sequencing depth of different samples, by default 4e7
|
|
245
|
+
lib_size_var : tuple of floats, optional
|
|
246
|
+
window of variability of sequencing depth of samples about 'depth',
|
|
247
|
+
by default (0.8, 1.2)
|
|
248
|
+
seed : int, optional
|
|
249
|
+
seed to ensure reproducible datasets, by default None
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
dictionary
|
|
254
|
+
counts: pandas DataFrame (n_genes * n_samples * n_groups) with count
|
|
255
|
+
data
|
|
256
|
+
params: pandas DataFrame (n_genes * 7) with identity of gene,
|
|
257
|
+
differential rhythmicity category, amplitude (A) in two groups, phases
|
|
258
|
+
(phi) in the two groups, and DE effect size.
|
|
259
|
+
exp_design: pandas DataFrame ((2*n_samples) * 2) with time labels for
|
|
260
|
+
individual samples in each group
|
|
261
|
+
|
|
262
|
+
Raises
|
|
263
|
+
------
|
|
264
|
+
ValueError
|
|
265
|
+
if 'reps' is not an int or has different length than 't'
|
|
266
|
+
FileNotFoundError
|
|
267
|
+
if file containing 'emp_dist' does not exist.
|
|
268
|
+
ValueError
|
|
269
|
+
if provided 'emp_dist' is invalid
|
|
270
|
+
|
|
271
|
+
Notes
|
|
272
|
+
-----
|
|
273
|
+
We extended the generative of model of differential gene expression of
|
|
274
|
+
Soneson & Delorenz [SD]_ to also include differentially rhythmic genes.
|
|
275
|
+
|
|
276
|
+
References
|
|
277
|
+
----------
|
|
278
|
+
.. [SD] Soneson C, Delorenzi M. A comparison of methods for differential
|
|
279
|
+
expression analysis of RNA-seq data. BMC Bioinformatics. 2013;14: 91.
|
|
280
|
+
"""
|
|
281
|
+
rng: Generator = np.random.default_rng(seed)
|
|
282
|
+
|
|
283
|
+
if not isinstance(reps, int) and (
|
|
284
|
+
not isinstance(reps, list) or len(reps) != len(t)
|
|
285
|
+
):
|
|
286
|
+
raise ValueError("Length of reps must be 1 or the same as length of t")
|
|
287
|
+
|
|
288
|
+
if isinstance(emp_dist, str):
|
|
289
|
+
match emp_dist:
|
|
290
|
+
case "liver":
|
|
291
|
+
emp_dist: pd.DataFrame = load_dataset("Mm_liver_LD_NC.csv.gz")
|
|
292
|
+
case "multitissue":
|
|
293
|
+
emp_dist: pd.DataFrame = load_dataset("Mm_multitissue_LD_ALF.csv.gz")
|
|
294
|
+
case _:
|
|
295
|
+
emp_dist: pd.DataFrame = load_dataset("Mm_liver_LD_NC.csv.gz")
|
|
296
|
+
elif not isinstance(emp_dist, pd.DataFrame):
|
|
297
|
+
raise ValueError("The provided emp_dist is invalid.")
|
|
298
|
+
|
|
299
|
+
if emp_dist.shape[1] > 2:
|
|
300
|
+
group_cols = list(pd.Index.difference(emp_dist.columns, ["size", "mu"]))
|
|
301
|
+
|
|
302
|
+
unique_groups = emp_dist[group_cols].drop_duplicates()
|
|
303
|
+
sampled_row = unique_groups.iloc[rng.choice(len(unique_groups))]
|
|
304
|
+
|
|
305
|
+
selected_key = (
|
|
306
|
+
sampled_row.iloc[0] if len(group_cols) == 1 else tuple(sampled_row)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
by_param = group_cols[0] if len(group_cols) == 1 else group_cols
|
|
310
|
+
emp_dist = emp_dist.groupby(by=by_param).get_group(selected_key)
|
|
311
|
+
|
|
312
|
+
t: NDArray = np.repeat(t, reps)
|
|
313
|
+
N: int = len(t)
|
|
314
|
+
|
|
315
|
+
G_rhy: NDArray = rng.uniform(size=n_genes) <= rhy_frac
|
|
316
|
+
prior_p = rng.gamma(shape=DR_probs, scale=1.0, size=4)
|
|
317
|
+
prior_p = prior_p / prior_p.sum()
|
|
318
|
+
DR_counts = rng.multinomial(G_rhy.sum(), prior_p, 1).squeeze()
|
|
319
|
+
DR_classes = np.array(["gain", "loss", "change", "same"])
|
|
320
|
+
DR_groups: NDArray = DR_classes[np.repeat(np.arange(4), DR_counts)]
|
|
321
|
+
|
|
322
|
+
A: NDArray = min_A_effect + rng.exponential(A_spread, (n_genes, 2))
|
|
323
|
+
A[~G_rhy, :] = 0.0
|
|
324
|
+
phi: NDArray = rng.uniform(size=(n_genes, 2)) * 2 * np.pi * G_rhy.reshape(-1, 1)
|
|
325
|
+
|
|
326
|
+
G_rhy_index: NDArray = np.where(G_rhy)[0]
|
|
327
|
+
for i in range(len(DR_groups)):
|
|
328
|
+
if DR_groups[i] == "gain":
|
|
329
|
+
A[G_rhy_index[i], 0] = 0.0
|
|
330
|
+
elif DR_groups[i] == "loss":
|
|
331
|
+
A[G_rhy_index[i], 1] = 0.0
|
|
332
|
+
elif DR_groups[i] == "same":
|
|
333
|
+
A[G_rhy_index[i], 0] = A[G_rhy_index[i], 1]
|
|
334
|
+
phi[G_rhy_index[i], 0] = phi[G_rhy_index[i], 1]
|
|
335
|
+
|
|
336
|
+
params: pd.DataFrame = pd.DataFrame(
|
|
337
|
+
{
|
|
338
|
+
"id": [f"g{i + 1}" for i in np.where(G_rhy)[0]],
|
|
339
|
+
"category": DR_groups,
|
|
340
|
+
"A_1": A[G_rhy, 0],
|
|
341
|
+
"A_2": A[G_rhy, 1],
|
|
342
|
+
"phi_1": phi[G_rhy, 0],
|
|
343
|
+
"phi_2": phi[G_rhy, 1],
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
params.columns: list[str] = [
|
|
347
|
+
"id",
|
|
348
|
+
"category",
|
|
349
|
+
"A_ctrl",
|
|
350
|
+
"A_expt",
|
|
351
|
+
"phi_ctrl",
|
|
352
|
+
"phi_expt",
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
t_pattern: NDArray = np.hstack(
|
|
356
|
+
[
|
|
357
|
+
np.cos(phi[:, [0]]) @ np.cos(2 * np.pi * t[None, :] / period)
|
|
358
|
+
+ np.sin(phi[:, [0]]) @ np.sin(2 * np.pi * t[None, :] / period),
|
|
359
|
+
np.cos(phi[:, [1]]) @ np.cos(2 * np.pi * t[None, :] / period)
|
|
360
|
+
+ np.sin(phi[:, [1]]) @ np.sin(2 * np.pi * t[None, :] / period),
|
|
361
|
+
]
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
G_DE: NDArray = rng.uniform(size=n_genes) <= DE_frac
|
|
365
|
+
DE_effects: NDArray = (
|
|
366
|
+
min_DE_effect + rng.exponential(DE_spread, n_genes)
|
|
367
|
+
) * np.sign(2 * rng.uniform(size=n_genes) - 1)
|
|
368
|
+
DE_effects[~G_DE] = 0.0
|
|
369
|
+
params_de = pd.DataFrame(
|
|
370
|
+
{
|
|
371
|
+
"id": [f"g{i + 1}" for i in np.where(G_DE)[0]],
|
|
372
|
+
"DE_effect": DE_effects[G_DE],
|
|
373
|
+
}
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
params: pd.DataFrame = pd.merge(
|
|
377
|
+
params, params_de, on="id", how="outer", validate="one_to_one"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
lib_size_fct: NDArray = rng.uniform(lib_size_var[0], lib_size_var[1], 2 * N)
|
|
381
|
+
lib_size: NDArray = lib_size_fct * depth
|
|
382
|
+
|
|
383
|
+
draw: NDArray = rng.choice(emp_dist.shape[0], n_genes, replace=True)
|
|
384
|
+
|
|
385
|
+
size: NDArray = emp_dist["size"].values[draw].reshape(-1, 1)
|
|
386
|
+
|
|
387
|
+
lambda_: NDArray = emp_dist["mu"].values[draw].reshape(-1, 1)
|
|
388
|
+
|
|
389
|
+
DE_effects: NDArray = np.hstack([np.ones((n_genes, 1)), DE_effects.reshape(-1, 1)])
|
|
390
|
+
|
|
391
|
+
lambda_: NDArray = np.repeat(lambda_, 2 * N, axis=1) * 2 ** (
|
|
392
|
+
DE_effects[:, np.repeat([0, 1], N)] + A[:, np.repeat([0, 1], N)] * t_pattern
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
mu: NDArray = lambda_ / (np.sum(lambda_, axis=0) / lib_size)
|
|
396
|
+
|
|
397
|
+
counts: NDArray = nbinom.rvs(n=size, p=size / (mu + size), random_state=seed)
|
|
398
|
+
|
|
399
|
+
exp_design: pd.DataFrame = pd.DataFrame(
|
|
400
|
+
{"time": np.tile(t, 2), "group": np.repeat(groups, N)},
|
|
401
|
+
index=pd.Index(
|
|
402
|
+
"".join(rng.choice(list("abcdefghijklmnopqrstuvwxyz"), 5))
|
|
403
|
+
for _ in range(2 * N)
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
"counts": pd.DataFrame(
|
|
409
|
+
counts,
|
|
410
|
+
index=pd.Index([f"g{i + 1}" for i in range(n_genes)]),
|
|
411
|
+
columns=exp_design.index,
|
|
412
|
+
),
|
|
413
|
+
"params": params,
|
|
414
|
+
"exp_design": exp_design,
|
|
415
|
+
}
|
pyfauxseq/utils.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Utility functions to downsample and normalize count data.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- drop_counts: to downsample counts to a certain depth
|
|
5
|
+
- downsample: to downsample counts in multiple samples to a common depth
|
|
6
|
+
- load_dataset: to load csv data
|
|
7
|
+
- normalize_counts: to normalize count data using median of ratios
|
|
8
|
+
- median_of_ratios: to estimate median of ratios for the samples
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import importlib.resources
|
|
12
|
+
import os
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
14
|
+
from concurrent.futures._base import Future
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
from numpy.typing import NDArray
|
|
19
|
+
|
|
20
|
+
from pyfauxseq import data
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def drop_counts(y: NDArray, N: int) -> NDArray[np.int_]:
|
|
24
|
+
"""Downsample count data for a single sample such that total counts equals N.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
y : array-like
|
|
29
|
+
count data for one sample
|
|
30
|
+
N : int
|
|
31
|
+
target total counts
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
array
|
|
36
|
+
downsampled counts
|
|
37
|
+
|
|
38
|
+
Raises
|
|
39
|
+
------
|
|
40
|
+
ValueError
|
|
41
|
+
if target counts exceeds actual sum of counts
|
|
42
|
+
"""
|
|
43
|
+
total: int = np.sum(y)
|
|
44
|
+
|
|
45
|
+
if total < N:
|
|
46
|
+
raise ValueError("N is larger than the total counts.")
|
|
47
|
+
|
|
48
|
+
if total == N:
|
|
49
|
+
return y
|
|
50
|
+
else:
|
|
51
|
+
all_reads: NDArray = np.repeat(np.arange(y.shape[0]), y)
|
|
52
|
+
removed: NDArray = np.random.choice(all_reads, N, replace=False)
|
|
53
|
+
return np.bincount(removed, minlength=y.shape[0])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def downsample(
|
|
57
|
+
counts: NDArray, parallel: bool = True, ncores: int | None = None
|
|
58
|
+
) -> NDArray[np.int_]:
|
|
59
|
+
"""Downsample multiple samples in parallel.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
counts : ndarray
|
|
64
|
+
count data to be downsampled (n_genes * n_samples)
|
|
65
|
+
parallel : bool, optional
|
|
66
|
+
should samples be processed in parallel, by default True
|
|
67
|
+
ncores : int | None, optional
|
|
68
|
+
number of cores to be used, by default None (use all available)
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
ndarray
|
|
73
|
+
downsampled count data matrix
|
|
74
|
+
"""
|
|
75
|
+
min_lib_size: float = np.min(np.sum(counts, axis=0))
|
|
76
|
+
|
|
77
|
+
if parallel:
|
|
78
|
+
if ncores is None:
|
|
79
|
+
ncores: int = min(32, (os.cpu_count() or 1) + 4)
|
|
80
|
+
with ThreadPoolExecutor(max_workers=ncores) as executor:
|
|
81
|
+
futures: list[Future] = [
|
|
82
|
+
executor.submit(drop_counts, counts[:, i], min_lib_size)
|
|
83
|
+
for i in range(counts.shape[1])
|
|
84
|
+
]
|
|
85
|
+
downsampled_counts: NDArray = np.column_stack(
|
|
86
|
+
[future.result() for future in as_completed(futures)]
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
downsampled_counts: NDArray = np.column_stack(
|
|
90
|
+
[drop_counts(counts[:, i], min_lib_size) for i in range(counts.shape[1])]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return downsampled_counts
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_dataset(filename: str) -> pd.DataFrame:
|
|
97
|
+
"""Read data from files (currently only csv supported).
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
filename : str
|
|
102
|
+
filename to be read
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
pandas DataFrame
|
|
107
|
+
file contents as a DataFrame
|
|
108
|
+
|
|
109
|
+
Raises
|
|
110
|
+
------
|
|
111
|
+
ValueError
|
|
112
|
+
if unsupported file format (not csv)
|
|
113
|
+
"""
|
|
114
|
+
with importlib.resources.path(data, filename) as data_path:
|
|
115
|
+
if ".csv" in filename:
|
|
116
|
+
return pd.read_csv(data_path)
|
|
117
|
+
else:
|
|
118
|
+
raise ValueError("Unsupported file format")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def normalize_counts(counts: pd.DataFrame, log: bool = True) -> pd.DataFrame:
|
|
122
|
+
"""Normalize counts using median of ratios with optional log transform.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
counts : pandas DataFrame
|
|
127
|
+
count data to be normalized
|
|
128
|
+
log : bool, optional
|
|
129
|
+
should the normalized counts be log2 transformed, by default True
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
pandas DataFrame
|
|
134
|
+
normalized count data
|
|
135
|
+
"""
|
|
136
|
+
norm_counts: pd.DataFrame = (
|
|
137
|
+
counts / np.sum(counts, axis=0) / median_of_ratios(counts) * 1e6
|
|
138
|
+
)
|
|
139
|
+
if log:
|
|
140
|
+
norm_counts: pd.DataFrame = np.log2(1 + norm_counts)
|
|
141
|
+
return norm_counts
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def median_of_ratios(counts: pd.DataFrame) -> NDArray:
|
|
145
|
+
"""Compute median of ratios for each sample.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
counts : pandas DataFrame
|
|
150
|
+
count data matrix (n_genes * n_samples)
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
ndarray
|
|
155
|
+
median of ratios
|
|
156
|
+
"""
|
|
157
|
+
counts: NDArray = counts.to_numpy()
|
|
158
|
+
return 2 ** np.ma.median(
|
|
159
|
+
np.ma.log2(counts) - np.ma.mean(np.ma.log2(counts), axis=1, keepdims=True),
|
|
160
|
+
axis=0,
|
|
161
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyfauxseq
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: This package can generate synthetic timeseries RNA-seq data with a fraction of rhythmic genes and empirical relationship between mean expression and variability across replicates of genes.
|
|
5
|
+
Project-URL: bugs, https://github.com/bharathananth/pyfauxseq/issues
|
|
6
|
+
Project-URL: homepage, https://github.com/bharathananth/pyfauxseq
|
|
7
|
+
Project-URL: documentation, https://bharathananth.github.io/pypfauxseq
|
|
8
|
+
Author-email: Bharath Ananthasubramaniam <bharath.ananthasubramaniam@hu-berlin.de>
|
|
9
|
+
Maintainer-email: Bharath Ananthasubramaniam <bharath.ananthasubramaniam@hu-berlin.de>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: numpy
|
|
21
|
+
Requires-Dist: pandas
|
|
22
|
+
Requires-Dist: scipy
|
|
23
|
+
Provides-Extra: docs
|
|
24
|
+
Requires-Dist: mkdocs; extra == 'docs'
|
|
25
|
+
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
26
|
+
Requires-Dist: mkdocstrings-python; extra == 'docs'
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: coverage; extra == 'test'
|
|
29
|
+
Requires-Dist: hypothesis; extra == 'test'
|
|
30
|
+
Requires-Dist: pytest; extra == 'test'
|
|
31
|
+
Requires-Dist: ruff; extra == 'test'
|
|
32
|
+
Requires-Dist: ty; extra == 'test'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# pyfauxseq
|
|
36
|
+
### A python Implementation of the R package fauxseq
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+

|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
## What pyfauxseq does
|
|
43
|
+
|
|
44
|
+
This package can generate synthetic timeseries RNA-seq data with a fraction of rhythmic genes and empirical relationship between mean expression and variability across replicates of genes.
|
|
45
|
+
|
|
46
|
+
`pyfauxseq` improves upon previous tools as follows:
|
|
47
|
+
- generates negative-binomial count data with empirically-estimated mean-dispersion properties.
|
|
48
|
+
- generates data with either differential expression or differential rhythmicity or both.
|
|
49
|
+
|
|
50
|
+
## How to install pyfauxseq
|
|
51
|
+
```python -m pip install pyfauxseq```
|
|
52
|
+
|
|
53
|
+
## Get started with pyfauxseq
|
|
54
|
+
Get started with synthetic RNA-seq data with the ground truth with the default parameters using
|
|
55
|
+
```python
|
|
56
|
+
import pyfauxseq as pf
|
|
57
|
+
sim_data = pf.generate_rhythmic_rnaseq()
|
|
58
|
+
sim_data["counts"] # the count data
|
|
59
|
+
sim_data["params"] # parameters of the rhythmic genes
|
|
60
|
+
sim_data["exp_design"] # time labels of the individual sample (columns) of count data
|
|
61
|
+
```
|
|
62
|
+
## How to cite pyfauxseq
|
|
63
|
+
Please cite this software using CITATION.CFF or the "Cite this repository" link in the right sidebar.
|
|
64
|
+
|
|
65
|
+
## Credits
|
|
66
|
+
|
|
67
|
+
This package was created with [Cookiecutter](https://github.com/audreyfeldroy/cookiecutter) and the [audreyfeldroy/cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage) project template.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyfauxseq/__init__.py,sha256=_okOASUW014T2OEVG00Ck1ByqSvSADvIM-TV8doxLBs,344
|
|
2
|
+
pyfauxseq/estimators.py,sha256=hZq57SGqzTbSkZ-sDW1DGxSZ54KS9LHSI1XYvEizw6k,2833
|
|
3
|
+
pyfauxseq/generate_rnaseq.py,sha256=QxrC_9CFyf8nPPfIVR17lQQwj0IBR0PSZ37A5gl2Fm0,15078
|
|
4
|
+
pyfauxseq/utils.py,sha256=dY8AvDfH-x8xWUEl9QXZvf-n6HaOFlgEdk5iygUOdxc,4373
|
|
5
|
+
pyfauxseq/data/Mm_liver_LD_NC.csv.gz,sha256=TVJpEUt4hhgw8J4RBo3nv5l0pWRVDU2Wjpa8CMSc3X0,179263
|
|
6
|
+
pyfauxseq/data/Mm_multitissue_LD_ALF.csv.gz,sha256=sD9oLmUj1NpUeDDRaH_k8wtY82MpxHPCw2WxnT9LhlI,615681
|
|
7
|
+
pyfauxseq/data/Mm_multitissue_LD_ALF.json.gz,sha256=yCGE5GMRf09D7uw-Q6kb8V5mo3DVklYra345PsVoZyg,307165
|
|
8
|
+
pyfauxseq/data/__init__.py,sha256=SnUIMg2StvU_Ut0Uskz98wRzyerh9HGionlsfoKrZHs,65
|
|
9
|
+
pyfauxseq-0.1.0.dist-info/METADATA,sha256=D12aWFs5HbPn78zzGgF30NMnHnKRdKrmpW-FYaL0ZLc,3107
|
|
10
|
+
pyfauxseq-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
pyfauxseq-0.1.0.dist-info/licenses/LICENSE,sha256=shsJDDj0UNAzs_W2RC9eOKWWAX8BYLpPQuOI6qElzeQ,1084
|
|
12
|
+
pyfauxseq-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Bharath Ananthasubramaniam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|