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 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
@@ -0,0 +1 @@
1
+ """Empirically estimated mean-dispersion data to sample from."""
@@ -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
+ ![PyPI - Version](https://img.shields.io/pypi/v/pyfauxseq)
39
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/pyfauxseq)
40
+ ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/bharathananth/pyfauxseq/total)
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.