bowside 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.
- bowside/__init__.py +6 -0
- bowside/dataset.py +535 -0
- bowside/mixture.py +408 -0
- bowside/mixture_claude.py +378 -0
- bowside/model.py +275 -0
- bowside/network.py +448 -0
- bowside/sampling.py +257 -0
- bowside/sensitivity.py +289 -0
- bowside/training.py +986 -0
- bowside/trajectory.py +327 -0
- bowside/utils.py +75 -0
- bowside-0.1.0.dist-info/METADATA +92 -0
- bowside-0.1.0.dist-info/RECORD +15 -0
- bowside-0.1.0.dist-info/WHEEL +4 -0
- bowside-0.1.0.dist-info/licenses/LICENSE +674 -0
bowside/mixture.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""Module for mixture models used in trajectory inference."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Collection
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from scipy.special import logsumexp
|
|
9
|
+
from scipy.stats import multivariate_normal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GaussianMixture:
|
|
13
|
+
"""
|
|
14
|
+
A multivariate Gaussian mixture model with frozen weights, means,
|
|
15
|
+
and covariances.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
weights : np.typing.ArrayLike
|
|
20
|
+
Array-like of shape (n_components,) containing the weights of
|
|
21
|
+
the mixture components.
|
|
22
|
+
means : np.typing.ArrayLike
|
|
23
|
+
Array-like of shape (n_components, n_var) containing the
|
|
24
|
+
mean vectors of the mixture components.
|
|
25
|
+
covariances : np.typing.ArrayLike
|
|
26
|
+
Array-like of shape (n_components, n_var, n_var)
|
|
27
|
+
containing the covariance matrices of the mixture components.
|
|
28
|
+
allow_singular : bool, optional
|
|
29
|
+
Whether to allow a singular covariance matrix. Default: False.
|
|
30
|
+
|
|
31
|
+
Raises
|
|
32
|
+
------
|
|
33
|
+
ValueError
|
|
34
|
+
An error is raised if any of the input arrays is of the wrong shape.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
weights: np.typing.ArrayLike,
|
|
40
|
+
means: np.typing.ArrayLike,
|
|
41
|
+
covariances: np.typing.ArrayLike,
|
|
42
|
+
allow_singular: bool = False,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.weights = np.asarray(weights, dtype=float)
|
|
45
|
+
self.means = np.asarray(means, dtype=float)
|
|
46
|
+
self.covariances = np.asarray(covariances, dtype=float)
|
|
47
|
+
|
|
48
|
+
# Check input
|
|
49
|
+
if self.weights.ndim != 1:
|
|
50
|
+
raise ValueError("Weights must be 1-dimensional.")
|
|
51
|
+
if self.means.ndim != 2:
|
|
52
|
+
raise ValueError("Means must be 2-dimensional.")
|
|
53
|
+
if self.covariances.ndim != 3:
|
|
54
|
+
raise ValueError("Covariances must be 3-dimensional.")
|
|
55
|
+
if np.any(self.weights < 0.0):
|
|
56
|
+
raise ValueError("All weights must be positive.")
|
|
57
|
+
if not np.isclose(self.weights.sum(), 1.0):
|
|
58
|
+
raise ValueError("Weights must sum to 1.")
|
|
59
|
+
if self.means.shape[0] != self.weights.shape[0]:
|
|
60
|
+
raise ValueError("Weights and means have mismatching shapes.")
|
|
61
|
+
if self.covariances.shape[0] != self.weights.shape[0]:
|
|
62
|
+
raise ValueError("Weights and covariances have mismatching shapes.")
|
|
63
|
+
if self.covariances.shape[1] != self.means.shape[1]:
|
|
64
|
+
raise ValueError("Means and covariances have mismatching shapes.")
|
|
65
|
+
if self.covariances.shape[1] != self.covariances.shape[2]:
|
|
66
|
+
raise ValueError("Covariance matrices are not square.")
|
|
67
|
+
|
|
68
|
+
# Initialize components
|
|
69
|
+
self.n_components = self.means.shape[0]
|
|
70
|
+
self.n_var = self.means.shape[1]
|
|
71
|
+
self.components = [
|
|
72
|
+
multivariate_normal(
|
|
73
|
+
mean=self.means[i],
|
|
74
|
+
cov=self.covariances[i],
|
|
75
|
+
allow_singular=allow_singular,
|
|
76
|
+
)
|
|
77
|
+
for i in range(self.n_components)
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def component_pdfs(self, X: np.typing.ArrayLike) -> np.ndarray:
|
|
81
|
+
"""
|
|
82
|
+
Evaluate the components' PDF for each sample.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
X : np.typing.ArrayLike
|
|
87
|
+
Array of shape (n_samples, n_var). A 1-dimensional array of
|
|
88
|
+
shape (n_var,) can be passed as well for a single sample.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
np.ndarray
|
|
93
|
+
Array of shape (n_samples, n_components) containing the PDF
|
|
94
|
+
value for each sample and component.
|
|
95
|
+
"""
|
|
96
|
+
return np.exp(self.component_log_pdfs(X))
|
|
97
|
+
|
|
98
|
+
def component_log_pdfs(self, X: np.typing.ArrayLike) -> np.ndarray:
|
|
99
|
+
"""
|
|
100
|
+
Evaluate the components' Log-PDF for each sample.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
X : np.typing.ArrayLike
|
|
105
|
+
Array of shape (n_samples, n_var). A 1-dimensional array of
|
|
106
|
+
shape (n_var,) can be passed as well for a single sample.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
np.ndarray
|
|
111
|
+
Array of shape (n_samples, n_components) containing the Log-
|
|
112
|
+
PDF value for each sample and component.
|
|
113
|
+
"""
|
|
114
|
+
X = np.asarray(X, dtype=float)
|
|
115
|
+
if X.ndim == 1:
|
|
116
|
+
X = X[None, :]
|
|
117
|
+
return np.column_stack([component.logpdf(X) for component in self.components])
|
|
118
|
+
|
|
119
|
+
def _weighted_component_log_densities(self, X: np.typing.ArrayLike) -> np.ndarray:
|
|
120
|
+
"""
|
|
121
|
+
Helper function for computing weighted log densitities.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
X : np.typing.ArrayLike
|
|
126
|
+
Array of shape (n_samples, n_var). A 1-dimensional array of
|
|
127
|
+
shape (n_var,) can be passed as well for a single sample.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
np.ndarray
|
|
132
|
+
Array of shape (n_samples, n_components) containing the
|
|
133
|
+
weighted log-densities for each sample and component.
|
|
134
|
+
"""
|
|
135
|
+
component_log_density = self.component_log_pdfs(X)
|
|
136
|
+
log_weights = np.full_like(self.weights, -np.inf)
|
|
137
|
+
positive_weights = self.weights > 0.0
|
|
138
|
+
log_weights[positive_weights] = np.log(self.weights[positive_weights])
|
|
139
|
+
return component_log_density + log_weights
|
|
140
|
+
|
|
141
|
+
def predict_proba(self, X: np.typing.ArrayLike) -> np.ndarray:
|
|
142
|
+
"""
|
|
143
|
+
Predict the components' probabilities for each sample.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
X : np.typing.ArrayLike
|
|
148
|
+
Array of shape (n_samples, n_var). A 1-dimensional array of
|
|
149
|
+
shape (n_var,) can be passed as well for a single sample.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
np.ndarray
|
|
154
|
+
Array of shape (n_samples, n_components) containing the
|
|
155
|
+
probabilities for each sample and component.
|
|
156
|
+
"""
|
|
157
|
+
weighted_log_density = self._weighted_component_log_densities(X)
|
|
158
|
+
return np.exp(
|
|
159
|
+
weighted_log_density
|
|
160
|
+
- logsumexp(weighted_log_density, axis=1, keepdims=True)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def score_samples(self, X: np.typing.ArrayLike) -> np.ndarray:
|
|
164
|
+
"""
|
|
165
|
+
Compute the log-likelihood of each sample.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
X : np.typing.ArrayLike
|
|
170
|
+
Array of shape (n_samples, n_var). A 1-dimensional array of
|
|
171
|
+
shape (n_var,) can be passed as well for a single sample.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
np.ndarray
|
|
176
|
+
Array of shape (n_samples, n_components) containing the
|
|
177
|
+
log-likelihood for each sample given the mixture model.
|
|
178
|
+
"""
|
|
179
|
+
scores = logsumexp(self._weighted_component_log_densities(X), axis=1)
|
|
180
|
+
assert isinstance(scores, np.ndarray)
|
|
181
|
+
return scores
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class GaussianMixtureTimeseries:
|
|
185
|
+
"""
|
|
186
|
+
A (pseudo)time series of Gaussian mixtures describing expression trajectories.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
sequences : np.typing.ArrayLike
|
|
191
|
+
Array of shape (n_steps, n_sequences, n_var) containing gene
|
|
192
|
+
expression sequences.
|
|
193
|
+
time_points : np.typing.ArrayLike
|
|
194
|
+
Array of shape (n_steps,) containing the time points.
|
|
195
|
+
cluster_labels : np.typing.ArrayLike
|
|
196
|
+
An array of shape (n_sequences). The value at index i is the
|
|
197
|
+
cluster number to which sequence i belongs.
|
|
198
|
+
allow_singular : bool, optional
|
|
199
|
+
Whether to allow a singular covariance matrix. Default: False.
|
|
200
|
+
|
|
201
|
+
Raises
|
|
202
|
+
------
|
|
203
|
+
ValueError
|
|
204
|
+
An error is raised if any of the input arrays is of the wrong shape.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(
|
|
208
|
+
self,
|
|
209
|
+
sequences: np.typing.ArrayLike,
|
|
210
|
+
time_points: np.typing.ArrayLike,
|
|
211
|
+
cluster_labels: np.typing.ArrayLike,
|
|
212
|
+
allow_singular: bool = False,
|
|
213
|
+
) -> None:
|
|
214
|
+
sequences = np.asarray(sequences)
|
|
215
|
+
time_points = np.asarray(time_points)
|
|
216
|
+
cluster_labels = np.asarray(cluster_labels)
|
|
217
|
+
|
|
218
|
+
# Input checks
|
|
219
|
+
if sequences.ndim != 3:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
"Parameter `sequences` must be 3-dimensional with shape "
|
|
222
|
+
"(n_steps, n_sequences, n_var)."
|
|
223
|
+
)
|
|
224
|
+
if time_points.ndim != 1:
|
|
225
|
+
raise ValueError("Parameter `time_points` must be 1-dimensional.")
|
|
226
|
+
if cluster_labels.ndim != 1:
|
|
227
|
+
raise ValueError("Parameter `cluster_labels` must be 1-dimensional.")
|
|
228
|
+
if time_points.shape[0] != sequences.shape[0]:
|
|
229
|
+
raise ValueError("Shape mismatch between `time_points` and `sequences`.")
|
|
230
|
+
if sequences.shape[1] != cluster_labels.shape[0]:
|
|
231
|
+
raise ValueError("Shape mismatch between `sequences` and `cluster_labels`.")
|
|
232
|
+
|
|
233
|
+
time_points.sort()
|
|
234
|
+
self.time_points = pd.Index(time_points)
|
|
235
|
+
|
|
236
|
+
# Compute component means
|
|
237
|
+
clusters, weights = np.unique(
|
|
238
|
+
cluster_labels,
|
|
239
|
+
return_counts=True,
|
|
240
|
+
)
|
|
241
|
+
self.weights = weights / weights.sum(keepdims=True)
|
|
242
|
+
|
|
243
|
+
# Compute cluster means and covariances for each time point
|
|
244
|
+
self.n_components = clusters.shape[0]
|
|
245
|
+
self.means = np.zeros(
|
|
246
|
+
(self.n_components, time_points.shape[0], sequences.shape[2]), dtype=float
|
|
247
|
+
)
|
|
248
|
+
self.covariances = np.zeros(
|
|
249
|
+
(
|
|
250
|
+
self.n_components,
|
|
251
|
+
time_points.shape[0],
|
|
252
|
+
sequences.shape[2],
|
|
253
|
+
sequences.shape[2],
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
for i, cluster in enumerate(clusters):
|
|
257
|
+
cluster_sequences = sequences[:, cluster_labels == cluster, :]
|
|
258
|
+
self.means[i] = cluster_sequences.mean(axis=1)
|
|
259
|
+
for t in range(time_points.shape[0]):
|
|
260
|
+
self.covariances[i, t, :, :] = np.cov(
|
|
261
|
+
cluster_sequences[t, :, :], bias=False, rowvar=False
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Generate Gaussian mixtures
|
|
265
|
+
self.gaussian_mixtures = [
|
|
266
|
+
GaussianMixture(
|
|
267
|
+
self.weights,
|
|
268
|
+
self.means[:, t, :],
|
|
269
|
+
self.covariances[:, t, :, :],
|
|
270
|
+
allow_singular=allow_singular,
|
|
271
|
+
)
|
|
272
|
+
for t in range(time_points.shape[0])
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
def component_pdfs(
|
|
276
|
+
self,
|
|
277
|
+
X: np.typing.ArrayLike,
|
|
278
|
+
time_points: Union[int, float, Collection[Union[int, float]]],
|
|
279
|
+
) -> np.ndarray:
|
|
280
|
+
"""
|
|
281
|
+
Evaluate the components' PDF for each sample, using the Gaussian
|
|
282
|
+
mixture of the respective time point.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
X : np.typing.ArrayLike
|
|
287
|
+
Array of shape (n_samples, n_var). A 1-dimensional array of
|
|
288
|
+
shape (n_var,) can be passed as well for a single sample.
|
|
289
|
+
time_points : Union[int, float, Collection[Union[int, float]]]
|
|
290
|
+
List containing the time points for each sample. If just one
|
|
291
|
+
sample is given, a single numeric value can be given for the
|
|
292
|
+
corresponding time point as well.
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
np.ndarray
|
|
297
|
+
Array of shape (n_samples, n_components) containing the PDF
|
|
298
|
+
value for each sample and component, given the time points.
|
|
299
|
+
|
|
300
|
+
Raises
|
|
301
|
+
------
|
|
302
|
+
ValueError
|
|
303
|
+
An error is raised if the number of time points does not
|
|
304
|
+
match the number of samples.
|
|
305
|
+
"""
|
|
306
|
+
X = np.asarray(X, dtype=float)
|
|
307
|
+
if X.ndim == 1:
|
|
308
|
+
X = X[None, :]
|
|
309
|
+
if isinstance(time_points, (int, float)):
|
|
310
|
+
time_points = [time_points]
|
|
311
|
+
if len(time_points) != X.shape[0]:
|
|
312
|
+
raise ValueError("Mismatching number of samples and time points.")
|
|
313
|
+
time_point_idx = np.asarray(
|
|
314
|
+
[self.time_points.get_loc(tp) for tp in time_points]
|
|
315
|
+
)
|
|
316
|
+
pdfs = np.zeros((X.shape[0], self.n_components))
|
|
317
|
+
for tp_index in np.unique(time_point_idx):
|
|
318
|
+
row_mask = time_point_idx == tp_index
|
|
319
|
+
pdfs[row_mask, :] = self.gaussian_mixtures[tp_index].component_pdfs(
|
|
320
|
+
X[row_mask, :]
|
|
321
|
+
)
|
|
322
|
+
return pdfs
|
|
323
|
+
|
|
324
|
+
def predict_proba(
|
|
325
|
+
self,
|
|
326
|
+
X: np.typing.ArrayLike,
|
|
327
|
+
time_points: Union[int, float, Collection[Union[int, float]]],
|
|
328
|
+
) -> np.ndarray:
|
|
329
|
+
"""
|
|
330
|
+
Predict the components' probabilities for each sample, using the
|
|
331
|
+
Gaussian mixture of the respective time point.
|
|
332
|
+
|
|
333
|
+
Parameters
|
|
334
|
+
----------
|
|
335
|
+
X : np.typing.ArrayLike
|
|
336
|
+
Array of shape (n_samples, n_var). A 1-dimensional array of
|
|
337
|
+
shape (n_var,) can be passed as well for a single sample.
|
|
338
|
+
time_points : Union[int, float, Collection[Union[int, float]]]
|
|
339
|
+
List containing the time points for each sample. If just one
|
|
340
|
+
sample is given, a single numeric value can be given for the
|
|
341
|
+
corresponding time point as well.
|
|
342
|
+
|
|
343
|
+
Returns
|
|
344
|
+
-------
|
|
345
|
+
np.ndarray
|
|
346
|
+
Array of shape (n_samples, n_components) containing the
|
|
347
|
+
probabilities for each sample and component, given the time
|
|
348
|
+
points.
|
|
349
|
+
"""
|
|
350
|
+
X = np.asarray(X, dtype=float)
|
|
351
|
+
if X.ndim == 1:
|
|
352
|
+
X = X[None, :]
|
|
353
|
+
if isinstance(time_points, (int, float)):
|
|
354
|
+
time_points = [time_points]
|
|
355
|
+
if len(time_points) != X.shape[0]:
|
|
356
|
+
raise ValueError("Mismatching number of samples and time points.")
|
|
357
|
+
time_point_idx = np.asarray(
|
|
358
|
+
[self.time_points.get_loc(tp) for tp in time_points]
|
|
359
|
+
)
|
|
360
|
+
probabilities = np.zeros((X.shape[0], self.n_components))
|
|
361
|
+
for tp_index in np.unique(time_point_idx):
|
|
362
|
+
row_mask = time_point_idx == tp_index
|
|
363
|
+
probabilities[row_mask, :] = self.gaussian_mixtures[tp_index].predict_proba(
|
|
364
|
+
X[row_mask, :]
|
|
365
|
+
)
|
|
366
|
+
return probabilities
|
|
367
|
+
|
|
368
|
+
def score_samples(
|
|
369
|
+
self,
|
|
370
|
+
X: np.typing.ArrayLike,
|
|
371
|
+
time_points: Union[int, float, Collection[Union[int, float]]],
|
|
372
|
+
) -> np.ndarray:
|
|
373
|
+
"""
|
|
374
|
+
Compute the log-likelihood of each sample, using the Gaussian
|
|
375
|
+
mixture of the respective time point.
|
|
376
|
+
|
|
377
|
+
Parameters
|
|
378
|
+
----------
|
|
379
|
+
X : np.typing.ArrayLike
|
|
380
|
+
Array of shape (n_samples, n_var). A 1-dimensional array of
|
|
381
|
+
shape (n_var,) can be passed as well for a single sample.
|
|
382
|
+
time_points : Union[int, float, Collection[Union[int, float]]]
|
|
383
|
+
List containing the time points for each sample. If just one
|
|
384
|
+
sample is given, a single numeric value can be given for the
|
|
385
|
+
corresponding time point as well.
|
|
386
|
+
|
|
387
|
+
Returns
|
|
388
|
+
-------
|
|
389
|
+
np.ndarray
|
|
390
|
+
Array of shape (n_samples, n_components) containing the
|
|
391
|
+
log-likelihood for each sample given the mixture model of
|
|
392
|
+
the respective time points.
|
|
393
|
+
"""
|
|
394
|
+
X = np.asarray(X, dtype=float)
|
|
395
|
+
if X.ndim == 1:
|
|
396
|
+
X = X[None, :]
|
|
397
|
+
if isinstance(time_points, (int, float)):
|
|
398
|
+
time_points = [time_points]
|
|
399
|
+
time_point_idx = np.asarray(
|
|
400
|
+
[self.time_points.get_loc(tp) for tp in time_points]
|
|
401
|
+
)
|
|
402
|
+
scores = np.zeros(X.shape[0])
|
|
403
|
+
for tp_index in np.unique(time_point_idx):
|
|
404
|
+
row_mask = time_point_idx == tp_index
|
|
405
|
+
scores[row_mask] = self.gaussian_mixtures[tp_index].score_samples(
|
|
406
|
+
X[row_mask, :]
|
|
407
|
+
)
|
|
408
|
+
return scores
|