clacell 2.1.2__tar.gz → 2.2.0__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.
- {clacell-2.1.2 → clacell-2.2.0}/PKG-INFO +2 -1
- clacell-2.2.0/clacell/__init__.py +6 -0
- clacell-2.2.0/clacell/conditional_classifier.py +421 -0
- {clacell-2.1.2 → clacell-2.2.0}/pyproject.toml +3 -2
- clacell-2.1.2/clacell/__init__.py +0 -5
- {clacell-2.1.2 → clacell-2.2.0}/.gitignore +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/LICENSE +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/README.md +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/_marker_based_annotation/__init__.py +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/_marker_based_annotation/cli.py +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/_marker_based_annotation/clustering.py +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/_marker_based_annotation/io.py +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/_marker_based_annotation/markers.py +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/_marker_based_annotation/model_selection.py +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/_marker_based_annotation/types.py +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/classifier.py +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/custom_stopper.py +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/marker_annotator.py +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/preprocessing.py +0 -0
- {clacell-2.1.2 → clacell-2.2.0}/clacell/test_robustness.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clacell
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: robust cell type classifier for immune cells
|
|
5
5
|
Project-URL: Homepage, https://github.com/Boolean-true/CLACELL
|
|
6
6
|
License-Expression: MIT
|
|
@@ -15,6 +15,7 @@ Requires-Dist: scikit-image>=0.26.0
|
|
|
15
15
|
Requires-Dist: scikit-learn>=1.8.0
|
|
16
16
|
Requires-Dist: scikit-optimize>=0.10.2
|
|
17
17
|
Requires-Dist: scipy>=1.17.1
|
|
18
|
+
Requires-Dist: torch>=2.13.0
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
19
20
|
|
|
20
21
|
# CLACELL
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .classifier import CellClassifier
|
|
2
|
+
from .conditional_classifier import ConditionalCellClassifier
|
|
3
|
+
from .marker_annotator import MarkerAnnotator
|
|
4
|
+
from .preprocessing import preprocess_data
|
|
5
|
+
|
|
6
|
+
__all__ = ["CellClassifier", "ConditionalCellClassifier", "MarkerAnnotator", "preprocess_data"]
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import scipy.stats as stats
|
|
3
|
+
from sklearn.base import BaseEstimator, ClassifierMixin
|
|
4
|
+
from sklearn.preprocessing import StandardScaler
|
|
5
|
+
from sklearn.model_selection import RandomizedSearchCV
|
|
6
|
+
from sklearn.preprocessing import OneHotEncoder
|
|
7
|
+
from sklearn.svm import LinearSVC
|
|
8
|
+
import torch
|
|
9
|
+
import torch.nn as nn
|
|
10
|
+
import torch.optim as optim
|
|
11
|
+
from torch.utils.data import DataLoader, TensorDataset
|
|
12
|
+
|
|
13
|
+
from .test_robustness import test_robustness
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConditionalDAE(nn.Module):
|
|
17
|
+
def __init__(self, input_dim, num_donors, latent_dim=128, noise_factor=0.3):
|
|
18
|
+
super(ConditionalDAE, self).__init__()
|
|
19
|
+
|
|
20
|
+
self.noise_factor = noise_factor
|
|
21
|
+
|
|
22
|
+
self.encoder = nn.Sequential(
|
|
23
|
+
nn.Linear(input_dim + num_donors, 256),
|
|
24
|
+
nn.BatchNorm1d(256),
|
|
25
|
+
nn.ReLU(),
|
|
26
|
+
nn.Dropout(0.1),
|
|
27
|
+
nn.Linear(256, latent_dim)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
self.decoder = nn.Sequential(
|
|
31
|
+
nn.Linear(latent_dim + num_donors, 256),
|
|
32
|
+
nn.BatchNorm1d(256),
|
|
33
|
+
nn.ReLU(),
|
|
34
|
+
nn.Dropout(0.1),
|
|
35
|
+
nn.Linear(256, input_dim)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def forward(self, x, cond):
|
|
39
|
+
if self.training:
|
|
40
|
+
noise = torch.randn_like(x) * self.noise_factor
|
|
41
|
+
x_noisy = x + noise
|
|
42
|
+
else:
|
|
43
|
+
x_noisy = x
|
|
44
|
+
|
|
45
|
+
# Combine Gene data and Donor ID for the Encoder
|
|
46
|
+
x_cond = torch.cat([x_noisy, cond], dim=1)
|
|
47
|
+
latent = self.encoder(x_cond)
|
|
48
|
+
|
|
49
|
+
# Combine Latent space and Donor ID for the Decoder
|
|
50
|
+
latent_cond = torch.cat([latent, cond], dim=1)
|
|
51
|
+
reconstructed = self.decoder(latent_cond)
|
|
52
|
+
|
|
53
|
+
return reconstructed, latent
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ScRNACVAEClassifier:
|
|
57
|
+
def __init__(self, cdae, classifier, scaler, num_donors):
|
|
58
|
+
self.cdae = cdae
|
|
59
|
+
self.classifier = classifier
|
|
60
|
+
self.scaler = scaler
|
|
61
|
+
self.num_donors = num_donors
|
|
62
|
+
self.cdae.eval()
|
|
63
|
+
self.device = next(cdae.parameters()).device
|
|
64
|
+
|
|
65
|
+
def _transform_to_latent(self, X):
|
|
66
|
+
if hasattr(X, "toarray"):
|
|
67
|
+
X = X.toarray()
|
|
68
|
+
if self.scaler is not None:
|
|
69
|
+
X = self.scaler.transform(X)
|
|
70
|
+
|
|
71
|
+
X_tensor = torch.tensor(X, dtype=torch.float32).to(self.device)
|
|
72
|
+
|
|
73
|
+
cond_dummy = torch.zeros((X_tensor.shape[0], self.num_donors), dtype=torch.float32).to(self.device)
|
|
74
|
+
|
|
75
|
+
with torch.no_grad():
|
|
76
|
+
_, latent_tensor = self.cdae(X_tensor, cond_dummy)
|
|
77
|
+
X_latent = latent_tensor.cpu().numpy()
|
|
78
|
+
|
|
79
|
+
return X_latent
|
|
80
|
+
|
|
81
|
+
def predict(self, X):
|
|
82
|
+
X_latent = self._transform_to_latent(X)
|
|
83
|
+
return self.classifier.predict(X_latent)
|
|
84
|
+
|
|
85
|
+
def predict_proba(self, X):
|
|
86
|
+
X_latent = self._transform_to_latent(X)
|
|
87
|
+
return self.classifier.predict_proba(X_latent)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ConditionalCellClassifier(BaseEstimator, ClassifierMixin):
|
|
91
|
+
def __init__(self, n_iter_search=50, random_state=None):
|
|
92
|
+
"""
|
|
93
|
+
Initializes the cell classififer.
|
|
94
|
+
"""
|
|
95
|
+
self.n_iter_search = n_iter_search
|
|
96
|
+
self.random_state = random_state
|
|
97
|
+
|
|
98
|
+
self.model = None
|
|
99
|
+
self.best_params_ = None
|
|
100
|
+
self.is_trained = False
|
|
101
|
+
self.genes_in_training_set = None
|
|
102
|
+
|
|
103
|
+
def random_search(
|
|
104
|
+
self,
|
|
105
|
+
X_train,
|
|
106
|
+
y_train,
|
|
107
|
+
donor_train,
|
|
108
|
+
X_test=None,
|
|
109
|
+
y_test=None,
|
|
110
|
+
donor_test=None,
|
|
111
|
+
labels="scumi-annotation",
|
|
112
|
+
n_jobs=1,
|
|
113
|
+
):
|
|
114
|
+
"""
|
|
115
|
+
Executes a hyperparameter tuning on the training set and returns the score on the test set.
|
|
116
|
+
Automatically followed by a final training with the best parameters.
|
|
117
|
+
"""
|
|
118
|
+
if not isinstance(X_train, pd.DataFrame):
|
|
119
|
+
raise ValueError("X_train must be a pandas DataFrame.")
|
|
120
|
+
|
|
121
|
+
if not isinstance(donor_train, pd.Series):
|
|
122
|
+
raise ValueError("donor_train must be a pandas Series.")
|
|
123
|
+
|
|
124
|
+
if X_test is not None:
|
|
125
|
+
if not isinstance(X_test, pd.DataFrame):
|
|
126
|
+
raise ValueError("X_test must be a pandas DataFrame.")
|
|
127
|
+
if y_test is None:
|
|
128
|
+
raise ValueError("y_test must be provided if X_test is provided.")
|
|
129
|
+
if donor_test is None:
|
|
130
|
+
raise ValueError("donor_test must be provided if X_test is provided.")
|
|
131
|
+
if not isinstance(donor_test, pd.Series):
|
|
132
|
+
raise ValueError("donor_test must be a pandas Series.")
|
|
133
|
+
|
|
134
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
135
|
+
|
|
136
|
+
if hasattr(X_train, "toarray"):
|
|
137
|
+
X_train = X_train.toarray()
|
|
138
|
+
if X_test is not None and hasattr(X_test, "toarray"):
|
|
139
|
+
X_test = X_test.toarray()
|
|
140
|
+
|
|
141
|
+
scaler = StandardScaler()
|
|
142
|
+
X_train_scaled = scaler.fit_transform(X_train)
|
|
143
|
+
|
|
144
|
+
oh_encoder = OneHotEncoder(sparse_output=False)
|
|
145
|
+
donor_train_oh = oh_encoder.fit_transform(donor_train.to_numpy().reshape(-1, 1))
|
|
146
|
+
|
|
147
|
+
num_donors = donor_train_oh.shape[1]
|
|
148
|
+
input_dim = X_train_scaled.shape[1]
|
|
149
|
+
latent_dim = 128
|
|
150
|
+
|
|
151
|
+
# Train DAE
|
|
152
|
+
input_dim = X_train_scaled.shape[1]
|
|
153
|
+
cdae = ConditionalDAE(input_dim, num_donors, latent_dim).to(device)
|
|
154
|
+
|
|
155
|
+
criterion = nn.MSELoss()
|
|
156
|
+
optimizer = optim.AdamW(cdae.parameters(), lr=1e-3, weight_decay=1e-4)
|
|
157
|
+
|
|
158
|
+
train_dataset = TensorDataset(
|
|
159
|
+
torch.tensor(X_train_scaled, dtype=torch.float32),
|
|
160
|
+
torch.tensor(donor_train_oh, dtype=torch.float32)
|
|
161
|
+
)
|
|
162
|
+
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
num_epochs = 150
|
|
166
|
+
best_loss = float('inf')
|
|
167
|
+
patience_counter = 0
|
|
168
|
+
patience = 5
|
|
169
|
+
delta_loss = 0.0002
|
|
170
|
+
cdae.train()
|
|
171
|
+
|
|
172
|
+
print("Start Conditional DAE Training...")
|
|
173
|
+
for epoch in range(num_epochs):
|
|
174
|
+
epoch_loss = 0.0
|
|
175
|
+
for x_batch, cond_batch in train_loader:
|
|
176
|
+
x_batch = x_batch.to(device)
|
|
177
|
+
cond_batch = cond_batch.to(device)
|
|
178
|
+
|
|
179
|
+
optimizer.zero_grad()
|
|
180
|
+
reconstructed, latent = cdae(x_batch, cond_batch)
|
|
181
|
+
|
|
182
|
+
loss = criterion(reconstructed, x_batch)
|
|
183
|
+
loss.backward()
|
|
184
|
+
optimizer.step()
|
|
185
|
+
epoch_loss += loss.item() * x_batch.size(0)
|
|
186
|
+
|
|
187
|
+
total_epoch_loss = epoch_loss / len(train_loader.dataset)
|
|
188
|
+
if (epoch + 1) % 10 == 0 or epoch == 0:
|
|
189
|
+
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {total_epoch_loss:.4f}")
|
|
190
|
+
|
|
191
|
+
# Early Stopping
|
|
192
|
+
if total_epoch_loss < best_loss - delta_loss:
|
|
193
|
+
best_loss = total_epoch_loss
|
|
194
|
+
patience_counter = 0
|
|
195
|
+
else:
|
|
196
|
+
patience_counter += 1
|
|
197
|
+
|
|
198
|
+
if patience_counter >= patience:
|
|
199
|
+
print(f"Early Stopping after [{epoch+1}/{num_epochs}] Epochs!")
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
cdae.eval()
|
|
203
|
+
print("\nExtract robust features...")
|
|
204
|
+
with torch.no_grad():
|
|
205
|
+
# Prepare training data for latent space extraction
|
|
206
|
+
X_train_tensor = torch.tensor(X_train_scaled, dtype=torch.float32).to(device)
|
|
207
|
+
cond_train_tensor = torch.tensor(donor_train_oh, dtype=torch.float32).to(device)
|
|
208
|
+
_, X_train_latent_tensor = cdae(X_train_tensor, cond_train_tensor)
|
|
209
|
+
X_train_latent = X_train_latent_tensor.cpu().numpy()
|
|
210
|
+
|
|
211
|
+
print("Start Hyperparametertuning...")
|
|
212
|
+
base_model = LinearSVC()
|
|
213
|
+
|
|
214
|
+
param_distributions = [
|
|
215
|
+
{
|
|
216
|
+
'C': stats.loguniform(1e-3, 2.0),
|
|
217
|
+
'penalty': ['l2'],
|
|
218
|
+
'dual': [True, False],
|
|
219
|
+
'class_weight': ['balanced', None],
|
|
220
|
+
'tol': stats.loguniform(1e-3, 1e-1)
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
'C': stats.loguniform(1e-3, 2.0),
|
|
224
|
+
'penalty': ['l1'],
|
|
225
|
+
'dual': [False],
|
|
226
|
+
'class_weight': ['balanced', None],
|
|
227
|
+
'tol': stats.loguniform(1e-3, 1e-1)
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
random_search = RandomizedSearchCV(
|
|
231
|
+
estimator=base_model,
|
|
232
|
+
param_distributions=param_distributions,
|
|
233
|
+
n_iter=self.n_iter_search,
|
|
234
|
+
cv=5,
|
|
235
|
+
scoring='accuracy',
|
|
236
|
+
n_jobs=n_jobs,
|
|
237
|
+
verbose=10
|
|
238
|
+
)
|
|
239
|
+
random_search.fit(X_train_latent, y_train)
|
|
240
|
+
|
|
241
|
+
self.best_params_ = random_search.best_params_
|
|
242
|
+
print(f"Best parameters found: {self.best_params_}")
|
|
243
|
+
best_model = random_search.best_estimator_
|
|
244
|
+
|
|
245
|
+
robust_model = ScRNACVAEClassifier(
|
|
246
|
+
cdae=cdae,
|
|
247
|
+
classifier=best_model,
|
|
248
|
+
scaler=scaler,
|
|
249
|
+
num_donors=num_donors
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
self.model = robust_model
|
|
253
|
+
self.is_trained = True
|
|
254
|
+
self.genes_in_training_set = X_train.columns.tolist()
|
|
255
|
+
|
|
256
|
+
if X_test is not None and y_test is not None:
|
|
257
|
+
# Compute Robustness score on test set with best parameters
|
|
258
|
+
self.evaluate(X_test, y_test, labels=labels)
|
|
259
|
+
|
|
260
|
+
# Automatically call train with best parameters on complete dataset after random search
|
|
261
|
+
print(
|
|
262
|
+
"\nStart final training with best parameters on complete training data..."
|
|
263
|
+
)
|
|
264
|
+
X = pd.concat([X_train, X_test], axis=0, ignore_index=True)
|
|
265
|
+
y = pd.concat([y_train, y_test], axis=0, ignore_index=True)
|
|
266
|
+
donors = pd.concat([donor_train, donor_test], axis=0, ignore_index=True)
|
|
267
|
+
self.train(X, y, donors, **self.best_params_)
|
|
268
|
+
|
|
269
|
+
def train(
|
|
270
|
+
self, X_train, y_train, donor_train, **hyperparameters
|
|
271
|
+
):
|
|
272
|
+
"""
|
|
273
|
+
Trains the model one the complete dataset with the given hyperparameters.
|
|
274
|
+
Can be either called automatically after random search or manually with custom hyperparameters.
|
|
275
|
+
"""
|
|
276
|
+
if not isinstance(X_train, pd.DataFrame):
|
|
277
|
+
raise ValueError("X_train must be a pandas DataFrame.")
|
|
278
|
+
|
|
279
|
+
if not isinstance(donor_train, pd.Series):
|
|
280
|
+
raise ValueError("donor_train must be a pandas Series.")
|
|
281
|
+
|
|
282
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
283
|
+
|
|
284
|
+
if hasattr(X_train, "toarray"):
|
|
285
|
+
X_train = X_train.toarray()
|
|
286
|
+
|
|
287
|
+
scaler = StandardScaler()
|
|
288
|
+
X_train_scaled = scaler.fit_transform(X_train)
|
|
289
|
+
|
|
290
|
+
oh_encoder = OneHotEncoder(sparse_output=False)
|
|
291
|
+
donor_train_oh = oh_encoder.fit_transform(donor_train.to_numpy().reshape(-1, 1))
|
|
292
|
+
|
|
293
|
+
num_donors = donor_train_oh.shape[1]
|
|
294
|
+
input_dim = X_train_scaled.shape[1]
|
|
295
|
+
latent_dim = 128
|
|
296
|
+
|
|
297
|
+
# Train DAE
|
|
298
|
+
input_dim = X_train_scaled.shape[1]
|
|
299
|
+
cdae = ConditionalDAE(input_dim, num_donors, latent_dim).to(device)
|
|
300
|
+
|
|
301
|
+
criterion = nn.MSELoss()
|
|
302
|
+
optimizer = optim.AdamW(cdae.parameters(), lr=1e-3, weight_decay=1e-4)
|
|
303
|
+
|
|
304
|
+
train_dataset = TensorDataset(
|
|
305
|
+
torch.tensor(X_train_scaled, dtype=torch.float32),
|
|
306
|
+
torch.tensor(donor_train_oh, dtype=torch.float32)
|
|
307
|
+
)
|
|
308
|
+
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
num_epochs = 150
|
|
312
|
+
best_loss = float('inf')
|
|
313
|
+
patience_counter = 0
|
|
314
|
+
patience = 5
|
|
315
|
+
delta_loss = 0.0002
|
|
316
|
+
cdae.train()
|
|
317
|
+
|
|
318
|
+
print("Start Conditional DAE Training...")
|
|
319
|
+
for epoch in range(num_epochs):
|
|
320
|
+
epoch_loss = 0.0
|
|
321
|
+
for x_batch, cond_batch in train_loader:
|
|
322
|
+
x_batch = x_batch.to(device)
|
|
323
|
+
cond_batch = cond_batch.to(device)
|
|
324
|
+
|
|
325
|
+
optimizer.zero_grad()
|
|
326
|
+
reconstructed, latent = cdae(x_batch, cond_batch)
|
|
327
|
+
|
|
328
|
+
loss = criterion(reconstructed, x_batch)
|
|
329
|
+
loss.backward()
|
|
330
|
+
optimizer.step()
|
|
331
|
+
epoch_loss += loss.item() * x_batch.size(0)
|
|
332
|
+
|
|
333
|
+
total_epoch_loss = epoch_loss / len(train_loader.dataset)
|
|
334
|
+
if (epoch + 1) % 10 == 0 or epoch == 0:
|
|
335
|
+
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {total_epoch_loss:.4f}")
|
|
336
|
+
|
|
337
|
+
# Early Stopping
|
|
338
|
+
if total_epoch_loss < best_loss - delta_loss:
|
|
339
|
+
best_loss = total_epoch_loss
|
|
340
|
+
patience_counter = 0
|
|
341
|
+
else:
|
|
342
|
+
patience_counter += 1
|
|
343
|
+
|
|
344
|
+
if patience_counter >= patience:
|
|
345
|
+
print(f"Early Stopping after [{epoch+1}/{num_epochs}] Epochs!")
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
cdae.eval()
|
|
349
|
+
print("\nExtract robust features...")
|
|
350
|
+
with torch.no_grad():
|
|
351
|
+
# Prepare training data for latent space extraction
|
|
352
|
+
X_train_tensor = torch.tensor(X_train_scaled, dtype=torch.float32).to(device)
|
|
353
|
+
cond_train_tensor = torch.tensor(donor_train_oh, dtype=torch.float32).to(device)
|
|
354
|
+
_, X_train_latent_tensor = cdae(X_train_tensor, cond_train_tensor)
|
|
355
|
+
X_train_latent = X_train_latent_tensor.cpu().numpy()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
base_model = LinearSVC(random_state=self.random_state, **hyperparameters)
|
|
359
|
+
base_model.fit(X_train_latent, y_train)
|
|
360
|
+
robust_model = ScRNACVAEClassifier(
|
|
361
|
+
cdae=cdae,
|
|
362
|
+
classifier=base_model,
|
|
363
|
+
scaler=scaler,
|
|
364
|
+
num_donors=num_donors
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
self.model = robust_model
|
|
368
|
+
self.is_trained = True
|
|
369
|
+
self.genes_in_training_set = X_train.columns.tolist()
|
|
370
|
+
|
|
371
|
+
def evaluate(
|
|
372
|
+
self,
|
|
373
|
+
X_test,
|
|
374
|
+
y_test,
|
|
375
|
+
labels="scumi-annotation",
|
|
376
|
+
X_ood=None,
|
|
377
|
+
y_ood=None,
|
|
378
|
+
feature_importances=None,
|
|
379
|
+
log_to_console=True,
|
|
380
|
+
log_to_file=True,
|
|
381
|
+
):
|
|
382
|
+
"""
|
|
383
|
+
Evaluates the model on the test set and returns the score.
|
|
384
|
+
If the model is not trained yet, it raises an error.
|
|
385
|
+
"""
|
|
386
|
+
if not self.is_trained:
|
|
387
|
+
raise RuntimeError(
|
|
388
|
+
"The model wasn't trained yet. Call 'train' or 'random_search' first."
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
print("Evaluate model on test data...")
|
|
392
|
+
return test_robustness(
|
|
393
|
+
self.model,
|
|
394
|
+
X_test,
|
|
395
|
+
y_test,
|
|
396
|
+
labels,
|
|
397
|
+
X_ood,
|
|
398
|
+
y_ood,
|
|
399
|
+
feature_importances,
|
|
400
|
+
log_to_console=log_to_console,
|
|
401
|
+
log_to_file=log_to_file,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
def predict(self, X):
|
|
405
|
+
"""
|
|
406
|
+
Infers new data with the trained model and returns the labels.
|
|
407
|
+
If the model is not trained yet, it raises an error.
|
|
408
|
+
"""
|
|
409
|
+
if not isinstance(X, pd.DataFrame):
|
|
410
|
+
raise ValueError("X must be a pandas DataFrame.")
|
|
411
|
+
|
|
412
|
+
if not self.is_trained:
|
|
413
|
+
raise RuntimeError(
|
|
414
|
+
"The model wasn't trained yet. Call 'train' or 'random_search' first."
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Filter genes that are not in the training set and reorder the remaining genes to match the training set
|
|
418
|
+
X = X.reindex(columns=self.genes_in_training_set, fill_value=0)
|
|
419
|
+
|
|
420
|
+
print("Infer new data...")
|
|
421
|
+
return self.model.predict(X)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "clacell"
|
|
3
3
|
license = "MIT"
|
|
4
|
-
version = "2.
|
|
4
|
+
version = "2.2.0"
|
|
5
5
|
description = "robust cell type classifier for immune cells"
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
requires-python = ">=3.13"
|
|
@@ -15,6 +15,7 @@ dependencies = [
|
|
|
15
15
|
"scikit-image>=0.26.0",
|
|
16
16
|
"igraph>=1.0.0",
|
|
17
17
|
"scikit-optimize>=0.10.2",
|
|
18
|
+
"torch>=2.13.0",
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
[build-system]
|
|
@@ -27,4 +28,4 @@ include = [
|
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
[project.urls]
|
|
30
|
-
"Homepage" = "https://github.com/Boolean-true/CLACELL"
|
|
31
|
+
"Homepage" = "https://github.com/Boolean-true/CLACELL"
|
|
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
|
|
File without changes
|