-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfactorization.py
More file actions
378 lines (305 loc) · 9.55 KB
/
Copy pathfactorization.py
File metadata and controls
378 lines (305 loc) · 9.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
import anndata
import cupy
import numpy as np
import pandas as pd
import scanpy as sc
import scipy.sparse as sps
from pacmap import PaCMAP
from parafac2.normalize import prepare_dataset
from parafac2.parafac2 import parafac2_nd, store_pf2
from scipy.stats import gmean
from sklearn.decomposition import PCA
from sklearn.linear_model import LinearRegression
from tensorly.cp_tensor import CPTensor
from tlviz.factor_tools import factor_match_score as fms
from tqdm import tqdm
def correct_conditions(X: anndata.AnnData):
"""Correct the conditions factors by overall read depth. Ensures that weighting is not affected by cell count difference"""
# sgIndex = X.obs["condition_unique_idxs"]
sgIndex = X.obs["condition_unique_idxs"].cat.codes
counts = np.zeros((np.amax(sgIndex) + 1, 1))
cond_mean = gmean(X.uns["Pf2_A"], axis=1)
x_count = X.X.sum(axis=1)
for ii in range(counts.size):
counts[ii] = np.sum(x_count[X.obs["condition_unique_idxs"] == ii])
lr = LinearRegression()
lr.fit(counts, cond_mean.reshape(-1, 1))
counts_correct = lr.predict(counts)
return X.uns["Pf2_A"] / counts_correct
def pf2(
X: anndata.AnnData,
rank: int,
random_state=1,
doEmbedding: bool = True,
tolerance=1e-9,
r2x=False,
):
cupy.cuda.Device(1).use()
pf_out, R2X = parafac2_nd(
X,
rank=rank,
random_state=random_state,
tol=tolerance,
n_iter_max=500,
)
X = store_pf2(X, pf_out)
if doEmbedding:
pcm = PaCMAP(random_state=random_state)
X.obsm["X_pf2_PaCMAP"] = pcm.fit_transform(X.obsm["projections"]) # type: ignore
if r2x:
return X, R2X
else:
return X
def pf2_pca_r2x(X: anndata.AnnData, ranks):
X = X.to_memory()
XX = sps.csr_array(X.X)
r2x_pf2 = np.zeros(len(ranks))
for i in tqdm(range(len(r2x_pf2)), total=len(r2x_pf2)):
_, R2X = parafac2_nd(X, rank=i + 1)
r2x_pf2[i] = R2X
pca = PCA(n_components=ranks[-1], svd_solver="arpack")
pca.fit(XX)
r2x_pca = np.cumsum(pca.explained_variance_ratio_)
return r2x_pf2, r2x_pca[np.array(ranks) - 1]
def calculateFMS(A: anndata.AnnData, B: anndata.AnnData):
"""Calculates FMS between 2 factors"""
factors = [A.uns["Pf2_A"], A.uns["Pf2_B"], A.varm["Pf2_C"]]
A_CP = CPTensor(
(
A.uns["Pf2_weights"],
factors,
)
)
factors = [B.uns["Pf2_A"], B.uns["Pf2_B"], B.varm["Pf2_C"]]
B_CP = CPTensor(
(
B.uns["Pf2_weights"],
factors,
)
)
return fms(A_CP, B_CP, consider_weights=False, skip_mode=1) # type: ignore
def fms_percent_drop(
X: anndata.AnnData,
percentList: np.ndarray,
runs: int,
rank: int = 30,
):
# Plots FMS score when percentage is removed from data
dataX = pf2(X, rank, doEmbedding=False)
fmsLists = []
for j in range(0, runs, 1):
scores = [1.0]
for i in percentList[1:]:
sampled_data: anndata.AnnData = sc.pp.subsample(
X, fraction=1 - (i / 100), random_state=j, copy=True
) # type: ignore
sampledX = pf2(sampled_data, rank, random_state=j + 2, doEmbedding=False)
fmsScore = calculateFMS(dataX, sampledX)
scores.append(fmsScore)
fmsLists.append(scores)
runsList_df = []
for i in range(0, runs):
for _j in range(0, len(percentList)):
runsList_df.append(i)
percentList_df = []
for _i in range(0, runs):
for j in range(0, len(percentList)):
percentList_df.append(percentList[j])
fmsList_df = []
for sublist in fmsLists:
fmsList_df += sublist
df = pd.DataFrame(
{
"Run": runsList_df,
"Percentage of Data Dropped": percentList_df,
"FMS": fmsList_df,
}
)
return df
def resample(data: anndata.AnnData) -> anndata.AnnData:
"""Bootstrapping dataset"""
indices = np.random.randint(0, data.shape[0], size=(data.shape[0],))
data = data[indices].copy()
return data
def fms_diff_ranks(
X: anndata.AnnData,
ranksList: list[int],
runs: int,
):
# Plots FMS when using different Pf2 components
fmsLists = []
for j in range(0, runs, 1):
scores = []
for i in ranksList:
dataX = pf2(X, rank=i, random_state=j, doEmbedding=False)
sampledX = pf2(resample(X), rank=i, random_state=j, doEmbedding=False)
fmsScore = calculateFMS(dataX, sampledX)
scores.append(fmsScore)
fmsLists.append(scores)
runsList_df = []
for i in range(0, runs):
for _j in range(0, len(ranksList)):
runsList_df.append(i)
ranksList_df = []
for _i in range(0, runs):
for j in range(0, len(ranksList)):
ranksList_df.append(ranksList[j])
fmsList_df = []
for sublist in fmsLists:
fmsList_df += sublist
df = pd.DataFrame(
{"Run": runsList_df, "Component": ranksList_df, "FMS": fmsList_df}
)
return df
def downsample_counts_multinomial(
X: anndata.AnnData,
percent_drop: float,
random_state: int = 0,
) -> anndata.AnnData:
"""
Create a downsampled counts copy of AnnData using multinomial sampling.
Parameters:
-----------
X : anndata.AnnData
Input dataset
percent_drop : float
Percentage of counts to drop (0-100)
random_state : int
Random seed for reproducibility
Returns:
--------
anndata.AnnData
Downsampled copy of the input data
"""
import scipy.sparse as sp
# Handle 0% drop case
if percent_drop == 0:
return X.copy()
# Set random seed
np.random.seed(random_state)
# Convert to CSR and extract structure
original_csr = X.X.tocsr()
data = original_csr.data.copy()
indices = original_csr.indices
indptr = original_csr.indptr
# Process each cell
for cell_idx in range(X.n_obs):
start_idx = indptr[cell_idx]
end_idx = indptr[cell_idx + 1]
if start_idx == end_idx:
continue
cell_data = data[start_idx:end_idx]
total_counts = int(np.sum(cell_data))
if total_counts == 0:
continue
new_total = int(total_counts * (1 - percent_drop / 100))
if new_total == 0:
data[start_idx:end_idx] = 0
continue
# Convert to probabilities and normalize
probs = cell_data / total_counts
probs = probs / np.sum(probs) # Ensure sum = 1.0
# Multinomial sampling
new_counts = np.random.multinomial(new_total, probs)
data[start_idx:end_idx] = new_counts.astype(cell_data.dtype)
# Create new sparse matrix
sampled_csr = sp.csr_matrix((data, indices, indptr), shape=original_csr.shape)
# Create new AnnData object
sampled_data = X.copy()
sampled_data.X = sampled_csr
return sampled_data
def calculate_fms_downsample(
X: anndata.AnnData,
X_pf2: anndata.AnnData,
percent_drop: float,
rank: int = 30,
deviance: bool = False,
condition: str = "Condition",
random_state: int = 0,
) -> float:
"""
Calculate FMS for a single downsampling scenario.
Parameters:
-----------
X : anndata.AnnData
Original dataset for reference
X_pf2 : anndata.AnnData
Full factorized dataset
percent_drop : float
Percentage of counts to drop (0-100)
rank : int
Factorization rank
deviance : bool
Whether to use deviance normalization
condition : str
Condition column name
random_state : int
Random seed
Returns:
--------
float
FMS score
"""
# Handle 0% drop case
if percent_drop == 0:
return 1.0
# Create downsampled data
sampled_data = downsample_counts_multinomial(
X, percent_drop, random_state=random_state
)
# Apply same processing as reference
sampled_data = prepare_dataset(
sampled_data, condition, geneThreshold=0.0, deviance=deviance
)
# Factorization
sampledX = pf2(sampled_data, rank, random_state=random_state + 2, doEmbedding=False)
return calculateFMS(X_pf2, sampledX)
def fms_percent_drop_counts(
X: anndata.AnnData,
percentList: np.ndarray,
rank: int = 30,
deviance: bool = False,
condition: str = "Condition",
geneThreshold: float = 0.0,
random_state: int = 0,
) -> pd.DataFrame:
"""
Calculate FMS for multiple downsampling percentages (single run).
Parameters:
-----------
X : anndata.AnnData
Input dataset
percentList : np.ndarray
Array of dropout percentages to test
rank : int
Factorization rank
deviance : bool
Whether to use deviance normalization
condition : str
Condition column name
geneThreshold : float
Gene threshold for preparation
random_state : int
Random seed
Returns:
--------
pd.DataFrame
DataFrame with columns: Percentage of Counts Dropped, FMS
"""
results = []
X_prepared = prepare_dataset(
X, condition, geneThreshold=geneThreshold, deviance=deviance
)
X_pf2 = pf2(X_prepared, rank, doEmbedding=False)
for percent_drop in percentList:
fms_score = calculate_fms_downsample(
X=X,
X_pf2=X_pf2,
percent_drop=percent_drop,
rank=rank,
deviance=deviance,
condition=condition,
random_state=random_state,
)
results.append({"Percentage of Counts Dropped": percent_drop, "FMS": fms_score})
return pd.DataFrame(results)