scmkl 0.1.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.
- scmkl-0.1.0/LICENSE +0 -0
- scmkl-0.1.0/PKG-INFO +14 -0
- scmkl-0.1.0/README.md +86 -0
- scmkl-0.1.0/scmkl/__init__.py +23 -0
- scmkl-0.1.0/scmkl/approx_kernels.py +83 -0
- scmkl-0.1.0/scmkl/data_processing.py +242 -0
- scmkl-0.1.0/scmkl/estimate_sigma.py +47 -0
- scmkl-0.1.0/scmkl/multimodal_processing.py +148 -0
- scmkl-0.1.0/scmkl/optimize_alpha.py +241 -0
- scmkl-0.1.0/scmkl/test.py +114 -0
- scmkl-0.1.0/scmkl/tfidf.py +94 -0
- scmkl-0.1.0/scmkl/train.py +52 -0
- scmkl-0.1.0/scmkl/utils.py +97 -0
- scmkl-0.1.0/scmkl.egg-info/PKG-INFO +14 -0
- scmkl-0.1.0/scmkl.egg-info/SOURCES.txt +18 -0
- scmkl-0.1.0/scmkl.egg-info/dependency_links.txt +1 -0
- scmkl-0.1.0/scmkl.egg-info/requires.txt +7 -0
- scmkl-0.1.0/scmkl.egg-info/top_level.txt +1 -0
- scmkl-0.1.0/setup.cfg +4 -0
- scmkl-0.1.0/setup.py +19 -0
scmkl-0.1.0/LICENSE
ADDED
|
File without changes
|
scmkl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: scmkl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Author: Sam Kupp, Ian VanGordon, Cigdem Ak
|
|
5
|
+
Author-email: kupp@ohsu.edu, vangordi@ohsu.edu, ak@ohsu.edu
|
|
6
|
+
Requires-Python: >3.11.1
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: wheel==0.41.2
|
|
9
|
+
Requires-Dist: anndata==0.10.8
|
|
10
|
+
Requires-Dist: celer==0.7.3
|
|
11
|
+
Requires-Dist: numpy==1.26.0
|
|
12
|
+
Requires-Dist: pandas==2.2.2
|
|
13
|
+
Requires-Dist: scikit-learn==1.3.2
|
|
14
|
+
Requires-Dist: scipy==1.14.1
|
scmkl-0.1.0/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
### scMKL
|
|
2
|
+
This is an introduction to single cell Multiple Kernel Learning. scMKL is a classification algorithm utilizing prior information to group features to enhance classification and aid understanding of distinguishing features in multi-omic data sets.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
# Packages needed to import data
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pickle
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
# This sys command allows us to import the scMKL_src module from any directory. '..' can be replaced by any path to the module
|
|
12
|
+
sys.path.insert(0, '..')
|
|
13
|
+
import src.scMKL_src as src
|
|
14
|
+
|
|
15
|
+
seed = np.random.default_rng(1)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
#### Inputs for scMKL
|
|
19
|
+
There are 4 required pieces of data (per modality) required for scMKL
|
|
20
|
+
- The data matrix itself with cells as rows and features as columns.
|
|
21
|
+
- Can be either a Numpy Array or Scipy Sparse array.
|
|
22
|
+
- The sample labels in a Numpy Array. To perform group lasso, these labels must be binary.
|
|
23
|
+
- Feature names in a Numpy Array. These are the names of the features corresponding with the data matrix
|
|
24
|
+
- A dictionary with grouping data. The keys are the names of the groups, and the values are the corresponding features.
|
|
25
|
+
- Example: {Group1: [feature1, feature2, feature3], Group2: [feature4, feature5, feature6], ...}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
X = np.load('./data/TCGA-ESCA.npy', allow_pickle = True)
|
|
30
|
+
labels = np.load('./data/TCGA-ESCA_cell_metadata.npy', allow_pickle = True)
|
|
31
|
+
features = np.load('./data/TCGA-ESCA_RNA_feature_names.npy', allow_pickle = True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
with open('./data/RNA_hallmark_groupings.pkl', 'rb') as fin:
|
|
35
|
+
group_dict = pickle.load(fin)
|
|
36
|
+
|
|
37
|
+
# This value for D, the number of fourier features in Z, was found to be optimal in previous literature. Generally increasing D increases accuracy, but runs slower.
|
|
38
|
+
D = int(np.sqrt(len(labels)) * np.log(np.log(len(labels))))
|
|
39
|
+
|
|
40
|
+
# Removes features in X and features that are not found in group_dict. Done to reduce memory usage and search time
|
|
41
|
+
X, features = src.Filter_Features(X, features, group_dict)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### Parameter Optimization
|
|
45
|
+
Kernel widths (sigma) are a parameter of the kernel approximation. Here we estimate sigma on a random 2000 samples from the training set before optimizing it with k-Fold Cross Validation on the full training set.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# The train/test sets are calculated to keep the proportion of each label the same in the training and testing sets.
|
|
50
|
+
train_indices, test_indices = src.Train_Test_Split(labels, seed_obj= seed)
|
|
51
|
+
|
|
52
|
+
X_train = X[train_indices,:]
|
|
53
|
+
X_test = X[test_indices,:]
|
|
54
|
+
y_train = labels[train_indices]
|
|
55
|
+
y_test = labels[test_indices]
|
|
56
|
+
|
|
57
|
+
sigmas = src.Estimate_Sigma(X= X_train, group_dict= group_dict, assay= 'rna', feature_set= features, seed_obj= seed)
|
|
58
|
+
|
|
59
|
+
sigmas = src.Optimize_Sigma(X = X_train, y = y_train, group_dict = group_dict, assay = 'rna', D = D, feature_set = features,
|
|
60
|
+
sigma_list = sigmas, k = 2, sigma_adjustments = np.arange(0.1,2,0.1), seed_obj= seed)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Calculating Z and Model Evaluation
|
|
64
|
+
Below, we calculate approximate kernels for each group in the grouping information.
|
|
65
|
+
|
|
66
|
+
Then we train the model to view the distinguishing feature groups between phenotypes and evaluate on a test set to quantify classification performance.
|
|
67
|
+
Looking at group normalized weights can reveal insights into underlying biology.
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
Z_train, Z_test = src.Calculate_Z(X_train= X_train, X_test= X_test, group_dict= group_dict, assay= 'rna', D= D, feature_set= features, sigma_list= sigmas, seed_obj= seed)
|
|
72
|
+
|
|
73
|
+
gl = src.Train_Model(Z_train, y_train, 2 * D)
|
|
74
|
+
predictions, metrics = src.Predict(gl, Z_test, y_test, metrics = ['AUROC', 'F1-Score', 'Accuracy', 'Precision', 'Recall'])
|
|
75
|
+
selected_groups = src.Find_Selected_Pathways(gl, group_names= group_dict.keys())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
print(metrics)
|
|
81
|
+
print(selected_groups)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
{'AUROC': 1.0, 'Accuracy': 1.0, 'F1-Score': 1.0, 'Precision': 1.0, 'Recall': 1.0}
|
|
85
|
+
['HALLMARK_ESTROGEN_RESPONSE_EARLY']
|
|
86
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
__all__ = ['approx_kernels', 'data_processing', 'estimate_sigma',
|
|
2
|
+
'multimodal_processing', 'optimize_alpha', 'test', 'tfidf',
|
|
3
|
+
'train', 'utils']
|
|
4
|
+
|
|
5
|
+
# from . import approx_kernels
|
|
6
|
+
# from . import data_processing
|
|
7
|
+
# from . import estimate_sigma
|
|
8
|
+
# from . import multimodal_processing
|
|
9
|
+
# from . import optimize_alpha
|
|
10
|
+
# from . import test
|
|
11
|
+
# from . import tfidf
|
|
12
|
+
# from . import train
|
|
13
|
+
# from . import utils
|
|
14
|
+
|
|
15
|
+
from scmkl.approx_kernels import *
|
|
16
|
+
from scmkl.data_processing import *
|
|
17
|
+
from scmkl.estimate_sigma import *
|
|
18
|
+
from scmkl.multimodal_processing import *
|
|
19
|
+
from scmkl.optimize_alpha import *
|
|
20
|
+
from scmkl.test import *
|
|
21
|
+
from scmkl.tfidf import *
|
|
22
|
+
from scmkl.train import *
|
|
23
|
+
from scmkl.utils import *
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy
|
|
3
|
+
|
|
4
|
+
from scmkl.data_processing import process_data
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def calculate_z(adata, n_features = 5000) -> tuple:
|
|
8
|
+
'''
|
|
9
|
+
Function to calculate approximate kernels.
|
|
10
|
+
Input:
|
|
11
|
+
adata- Adata obj as created by `Create_Adata`
|
|
12
|
+
Sigma can be calculated with Estimate_Sigma or a heuristic but must be positive.
|
|
13
|
+
n_features- Number of random feature to use when calculating Z- used for scalability
|
|
14
|
+
Output:
|
|
15
|
+
adata_object with Z matrices accessible with- adata.uns['Z_train'] and adata.uns['Z_test'] respectively
|
|
16
|
+
|
|
17
|
+
'''
|
|
18
|
+
assert np.all(adata.uns['sigma'] > 0), 'Sigma must be positive'
|
|
19
|
+
|
|
20
|
+
#Number of groupings taking from group_dict
|
|
21
|
+
N_pathway = len(adata.uns['group_dict'].keys())
|
|
22
|
+
D = adata.uns['D']
|
|
23
|
+
|
|
24
|
+
# Create Arrays to store concatenated group Z. Each group of features will have a corresponding entry in each array
|
|
25
|
+
Z_train = np.zeros((len(adata.uns['train_indices']), 2 * adata.uns['D'] * N_pathway))
|
|
26
|
+
Z_test = np.zeros((len(adata.uns['test_indices']), 2 * adata.uns['D'] * N_pathway))
|
|
27
|
+
|
|
28
|
+
# Loop over each of the groups and creating Z for each
|
|
29
|
+
for m, group_features in enumerate(adata.uns['group_dict'].values()):
|
|
30
|
+
|
|
31
|
+
#Extract features from mth group
|
|
32
|
+
num_group_features = len(group_features)
|
|
33
|
+
|
|
34
|
+
# group_feature_indices = adata.uns['seed_obj'].integers(low = 0, high = num_group_features, size = np.min([n_features, num_group_features]))
|
|
35
|
+
# group_features = np.array(list(group_features))[group_feature_indices]
|
|
36
|
+
group_features = adata.uns['seed_obj'].choice(np.array(list(group_features)), np.min([n_features, num_group_features]), replace = False)
|
|
37
|
+
|
|
38
|
+
# Create data arrays containing only features within this group
|
|
39
|
+
X_train = adata[adata.uns['train_indices'],:][:, group_features].X
|
|
40
|
+
X_test = adata[adata.uns['test_indices'],:][:, group_features].X
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
X_train, X_test = process_data(X_train = X_train, X_test = X_test, data_type = adata.uns['data_type'], return_dense = True)
|
|
44
|
+
|
|
45
|
+
#Extract pre-calculated sigma used for approximating kernel
|
|
46
|
+
adjusted_sigma = adata.uns['sigma'][m]
|
|
47
|
+
|
|
48
|
+
#Calculates approximate kernel according to chosen kernel function- may add more functions in the future
|
|
49
|
+
#Distribution data comes from Fourier Transform of original kernel function
|
|
50
|
+
if adata.uns['kernel_type'].lower() == 'gaussian':
|
|
51
|
+
|
|
52
|
+
gamma = 1/(2*adjusted_sigma**2)
|
|
53
|
+
sigma_p = 0.5*np.sqrt(2*gamma)
|
|
54
|
+
|
|
55
|
+
W = adata.uns['seed_obj'].normal(0, sigma_p, X_train.shape[1]*D).reshape((X_train.shape[1]),D)
|
|
56
|
+
|
|
57
|
+
elif adata.uns['kernel_type'].lower() == 'laplacian':
|
|
58
|
+
|
|
59
|
+
gamma = 1/(2*adjusted_sigma)
|
|
60
|
+
|
|
61
|
+
W = gamma * adata.uns['seed_obj'].standard_cauchy(X_train.shape[1]*D).reshape((X_train.shape[1],D))
|
|
62
|
+
|
|
63
|
+
elif adata.uns['kernel_type'].lower() == 'cauchy':
|
|
64
|
+
|
|
65
|
+
gamma = 1/(2*adjusted_sigma**2)
|
|
66
|
+
b = 0.5*np.sqrt(gamma)
|
|
67
|
+
|
|
68
|
+
W = adata.uns['seed_obj'].laplace(0, b, X_train.shape[1]*D).reshape((X_train.shape[1],D))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
train_projection = np.matmul(X_train, W)
|
|
72
|
+
test_projection = np.matmul(X_test, W)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
#Store group Z in whole-Z object. Preserves order to be able to extract meaningful groups
|
|
76
|
+
Z_train[0:, np.arange( m * 2 * D , (m + 1) * 2 * D)] = np.sqrt(1/D)*np.hstack((np.cos(train_projection), np.sin(train_projection)))
|
|
77
|
+
Z_test[0:, np.arange( m * 2 * D , (m + 1) * 2 * D)] = np.sqrt(1/D)*np.hstack((np.cos(test_projection), np.sin(test_projection)))
|
|
78
|
+
|
|
79
|
+
adata.uns['Z_train'] = Z_train
|
|
80
|
+
adata.uns['Z_test'] = Z_test
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
return adata
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import tracemalloc
|
|
2
|
+
import numpy as np
|
|
3
|
+
import scipy
|
|
4
|
+
import anndata as ad
|
|
5
|
+
import gc
|
|
6
|
+
|
|
7
|
+
from scmkl.tfidf import tfidf_filter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def filter_features(X, feature_names, group_dict):
|
|
11
|
+
'''
|
|
12
|
+
Function to remove unused features from X matrix. Any features not included in group_dict will be removed from the matrix.
|
|
13
|
+
Also puts the features in the same relative order (of included features)
|
|
14
|
+
Input:
|
|
15
|
+
X- Data array. Can be Numpy array or Scipy Sparse Array
|
|
16
|
+
feature_names- Numpy array of corresponding feature names
|
|
17
|
+
group_dict- Dictionary containing feature grouping information.
|
|
18
|
+
Example: {geneset: np.array(gene_1, gene_2, ..., gene_n)}
|
|
19
|
+
Output:
|
|
20
|
+
X- Data array containing data only for features in the group_dict
|
|
21
|
+
feature_names- Numpy array of corresponding feature names from group_dict
|
|
22
|
+
'''
|
|
23
|
+
assert X.shape[1] == len(feature_names), 'Given features do not correspond with features in X'
|
|
24
|
+
|
|
25
|
+
group_features = set()
|
|
26
|
+
feature_set = set(feature_names)
|
|
27
|
+
|
|
28
|
+
# Store all objects in dictionary in array
|
|
29
|
+
for group in group_dict.keys():
|
|
30
|
+
group_features.update(set(group_dict[group]))
|
|
31
|
+
|
|
32
|
+
group_dict[group] = np.sort(np.array(list(feature_set.intersection(set(group_dict[group])))))
|
|
33
|
+
|
|
34
|
+
# Find location of desired features in whole feature set
|
|
35
|
+
group_feature_indices = np.where(np.in1d(feature_names, np.array(list(group_features)), assume_unique = True))[0]
|
|
36
|
+
|
|
37
|
+
# Subset only the desired features and their data
|
|
38
|
+
X = X[:,group_feature_indices]
|
|
39
|
+
feature_names = np.array(list(feature_names))[group_feature_indices]
|
|
40
|
+
|
|
41
|
+
return X, feature_names, group_dict
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def train_test_split(y, train_indices = None, seed_obj = np.random.default_rng(100), train_ratio = 0.8):
|
|
45
|
+
'''
|
|
46
|
+
Function to calculate training and testing indices for given dataset. If train indices are given, it will calculate the test indices.
|
|
47
|
+
If train_indices == None, then it calculates both indices, preserving the ratio of each label in y
|
|
48
|
+
Input:
|
|
49
|
+
y- Numpy array of cell labels. Can have any number of classes for this function.
|
|
50
|
+
train_indices- Optional array of pre-determined training indices
|
|
51
|
+
seed_obj- Numpy random state used for random processes. Can be specified for reproducubility or set by default.
|
|
52
|
+
train_ratio- decimal value ratio of features in training:testing sets
|
|
53
|
+
Output:
|
|
54
|
+
train_indices- Array of indices of training cells
|
|
55
|
+
test_indices- Array of indices of testing cells
|
|
56
|
+
'''
|
|
57
|
+
|
|
58
|
+
# If train indices aren't provided
|
|
59
|
+
if train_indices == None:
|
|
60
|
+
|
|
61
|
+
unique_labels = np.unique(y)
|
|
62
|
+
train_indices = []
|
|
63
|
+
|
|
64
|
+
for label in unique_labels:
|
|
65
|
+
|
|
66
|
+
# Find index of each unique label
|
|
67
|
+
label_indices = np.where(y == label)[0]
|
|
68
|
+
|
|
69
|
+
# Sample these indices according to train ratio
|
|
70
|
+
train_label_indices = seed_obj.choice(label_indices, int(len(label_indices) * train_ratio), replace = False)
|
|
71
|
+
train_indices.extend(train_label_indices)
|
|
72
|
+
else:
|
|
73
|
+
assert len(train_indices) <= len(y), 'More train indices than there are samples'
|
|
74
|
+
|
|
75
|
+
train_indices = np.array(train_indices)
|
|
76
|
+
|
|
77
|
+
# Test indices are the indices not in the train_indices
|
|
78
|
+
test_indices = np.setdiff1d(np.arange(len(y)), train_indices, assume_unique = True)
|
|
79
|
+
|
|
80
|
+
return train_indices, test_indices
|
|
81
|
+
|
|
82
|
+
def sparse_var(X, axis = None):
|
|
83
|
+
|
|
84
|
+
'''
|
|
85
|
+
Function to calculate variance on a sparse matrix.
|
|
86
|
+
Input:
|
|
87
|
+
X- A scipy sparse or numpy array
|
|
88
|
+
axis- Determines which axis variance is calculated on. Same usage as Numpy
|
|
89
|
+
axis = 0 => column variances
|
|
90
|
+
axis = 1 => row variances
|
|
91
|
+
axis = None => total variance (calculated on all data)
|
|
92
|
+
Output:
|
|
93
|
+
var- Variance values calculated over the given axis
|
|
94
|
+
'''
|
|
95
|
+
|
|
96
|
+
# E[X^2] - E[X]^2
|
|
97
|
+
if scipy.sparse.issparse(X):
|
|
98
|
+
var = np.array((X.power(2).mean(axis = axis)) - np.square(X.mean(axis = axis)))
|
|
99
|
+
else:
|
|
100
|
+
var = np.var(X, axis = axis)
|
|
101
|
+
return var.ravel()
|
|
102
|
+
|
|
103
|
+
def process_data(X_train, X_test = None, data_type = 'counts', return_dense = True):
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
'''
|
|
107
|
+
Function to preprocess data matrix according to type of data (counts- e.g. rna, or binary- atac)
|
|
108
|
+
Will process test data according to parameters calculated from test data
|
|
109
|
+
|
|
110
|
+
Input:
|
|
111
|
+
X_train- A scipy sparse or numpy array
|
|
112
|
+
X_train- A scipy sparse or numpy array
|
|
113
|
+
data_type- 'counts' or 'binary'. Determines what preprocessing is applied to the data.
|
|
114
|
+
Log transforms and standard scales counts data
|
|
115
|
+
TFIDF filters ATAC data to remove uninformative columns
|
|
116
|
+
Output:
|
|
117
|
+
X_train, X_test- Numpy arrays with the process train/test data respectively.
|
|
118
|
+
'''
|
|
119
|
+
|
|
120
|
+
# Remove features that have no variance in the training data (will be uniformative)
|
|
121
|
+
assert data_type in ['counts', 'binary'], 'Improper value given for data_type'
|
|
122
|
+
|
|
123
|
+
if X_test == None:
|
|
124
|
+
X_test = X_train[:1,:] # Creates dummy matrix to for the sake of calculation without increasing computational time
|
|
125
|
+
orig_test = None
|
|
126
|
+
else:
|
|
127
|
+
orig_test = 'given'
|
|
128
|
+
|
|
129
|
+
var = sparse_var(X_train, axis = 0)
|
|
130
|
+
variable_features = np.where(var > 1e-5)[0]
|
|
131
|
+
|
|
132
|
+
X_train = X_train[:,variable_features]
|
|
133
|
+
X_test = X_test[:, variable_features]
|
|
134
|
+
|
|
135
|
+
#Data processing according to data type
|
|
136
|
+
if data_type.lower() == 'counts':
|
|
137
|
+
|
|
138
|
+
if scipy.sparse.issparse(X_train):
|
|
139
|
+
X_train = X_train.log1p()
|
|
140
|
+
X_test = X_test.log1p()
|
|
141
|
+
else:
|
|
142
|
+
X_train = np.log1p(X_train)
|
|
143
|
+
X_test = np.log1p(X_test)
|
|
144
|
+
|
|
145
|
+
#Center and scale count data
|
|
146
|
+
train_means = np.mean(X_train, 0)
|
|
147
|
+
train_sds = np.sqrt(var[variable_features])
|
|
148
|
+
|
|
149
|
+
X_train = (X_train - train_means) / train_sds
|
|
150
|
+
X_test = (X_test - train_means) / train_sds
|
|
151
|
+
|
|
152
|
+
elif data_type.lower() == 'binary':
|
|
153
|
+
|
|
154
|
+
# TFIDF filter binary peaks
|
|
155
|
+
non_empty_row = np.where(np.sum(X_train, axis = 1) > 0)[0]
|
|
156
|
+
|
|
157
|
+
if scipy.sparse.issparse(X_train):
|
|
158
|
+
non_0_cols = tfidf_filter(X_train.toarray()[non_empty_row,:], mode= 'filter')
|
|
159
|
+
else:
|
|
160
|
+
non_0_cols = tfidf_filter(X_train[non_empty_row,:], mode = 'filter')
|
|
161
|
+
|
|
162
|
+
X_train = X_train[:, non_0_cols]
|
|
163
|
+
X_test = X_test[:, non_0_cols]
|
|
164
|
+
|
|
165
|
+
if return_dense and scipy.sparse.issparse(X_train):
|
|
166
|
+
X_train = X_train.toarray()
|
|
167
|
+
X_test = X_test.toarray()
|
|
168
|
+
|
|
169
|
+
if orig_test == None:
|
|
170
|
+
return X_train
|
|
171
|
+
else:
|
|
172
|
+
return X_train, X_test
|
|
173
|
+
|
|
174
|
+
def create_adata(X, feature_names: np.ndarray, cell_labels: np.ndarray, group_dict: dict, data_type: str, split_data = None, D = 100,
|
|
175
|
+
remove_features = False, distance_metric = 'euclidean', kernel_type = 'Gaussian', random_state = 1):
|
|
176
|
+
|
|
177
|
+
'''
|
|
178
|
+
Function to create an AnnData object to carry all relevant information going forward
|
|
179
|
+
|
|
180
|
+
Input:
|
|
181
|
+
X- A data matrix of cells by features can be a numpy array, scipy sparse array or pandas dataframe (sparse array recommended for large datasets)
|
|
182
|
+
feature_names- A numpy array of feature names corresponding with the features in X
|
|
183
|
+
cell_labels- A numpy array of cell phenotypes corresponding with the cells in X. Must be binary
|
|
184
|
+
group_dict- Dictionary containing feature grouping information.
|
|
185
|
+
Example: {geneset: np.array(gene_1, gene_2, ..., gene_n)}
|
|
186
|
+
data_type- 'counts' or 'binary'. Determines what preprocessing is applied to the data.
|
|
187
|
+
Log transforms and standard scales counts data
|
|
188
|
+
TFIDF filters binary data
|
|
189
|
+
split_data- Either numpy array of precalculated train/test split for the cells -or-
|
|
190
|
+
None. If None, the train test split will be calculated with balanced classes.
|
|
191
|
+
D- Number of Random Fourier Features used to calculate Z. Should be a positive integer.
|
|
192
|
+
Higher values of D will increase classification accuracy at the cost of computation time
|
|
193
|
+
remove_features- Bool whether to filter the features from the dataset.
|
|
194
|
+
Will remove features from X, feature_names not in group_dict and remove features from groupings not in feature_names
|
|
195
|
+
random_state- Integer random_state used to set the seed for reproducibilty.
|
|
196
|
+
Output:
|
|
197
|
+
AnnData object with:
|
|
198
|
+
Data equal to the values in X- accessible with adata.X
|
|
199
|
+
Variable names equal to the values in feature_names- accessible with adata.var_names
|
|
200
|
+
Cell phenotypes equal to the values in cell_labels- accessible with adata.obs['labels']
|
|
201
|
+
Train/test split either as given or calculated in this function- accessible with adata.uns['train_indices'] and adata.uns['test_indices'] respectively
|
|
202
|
+
Grouping information equal to given group_dict- accessible with adata.uns['group_dict']
|
|
203
|
+
seed_obj with seed equal to 100 * random_state- accessible with adata.uns['seed_obj']
|
|
204
|
+
D- accessible with adata.uns['D']
|
|
205
|
+
Type of data to determine preprocessing steps- accessible with adata.uns['data_type']
|
|
206
|
+
|
|
207
|
+
'''
|
|
208
|
+
|
|
209
|
+
assert X.shape[0] == len(cell_labels), 'Different number of cells than labels'
|
|
210
|
+
assert X.shape[1] == len(feature_names), 'Different number of features in X than feature names'
|
|
211
|
+
assert len(np.unique(cell_labels)) == 2, 'cell_labels must contain 2 classes'
|
|
212
|
+
assert data_type in ['counts', 'binary'], 'data_type must be either "counts" or "binary"'
|
|
213
|
+
assert isinstance(D, int) and D > 0, 'D must be a positive integer'
|
|
214
|
+
assert kernel_type.lower() in ['gaussian', 'laplacian', 'cauchy'], 'Given kernel type not implemented. Gaussian, Laplacian, and Cauchy are the acceptable types.'
|
|
215
|
+
|
|
216
|
+
if remove_features:
|
|
217
|
+
X, feature_names, group_dict = filter_features(X, feature_names, group_dict)
|
|
218
|
+
|
|
219
|
+
adata = ad.AnnData(X)
|
|
220
|
+
adata.var_names = feature_names
|
|
221
|
+
adata.obs['labels'] = cell_labels
|
|
222
|
+
adata.uns['group_dict'] = group_dict
|
|
223
|
+
adata.uns['seed_obj'] = np.random.default_rng(100 * random_state)
|
|
224
|
+
adata.uns['data_type'] = data_type
|
|
225
|
+
adata.uns['D'] = D
|
|
226
|
+
adata.uns['kernel_type'] = kernel_type
|
|
227
|
+
adata.uns['distance_metric'] = distance_metric
|
|
228
|
+
|
|
229
|
+
if split_data == None:
|
|
230
|
+
train_indices, test_indices = train_test_split(cell_labels, seed_obj = adata.uns['seed_obj'])
|
|
231
|
+
else:
|
|
232
|
+
train_indices = np.where(split_data == 'train')[0]
|
|
233
|
+
test_indices = np.where(split_data == 'test')[0]
|
|
234
|
+
|
|
235
|
+
adata.uns['train_indices'] = train_indices
|
|
236
|
+
adata.uns['test_indices'] = test_indices
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
return adata
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy
|
|
3
|
+
|
|
4
|
+
from scmkl.data_processing import process_data
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def estimate_sigma(adata, n_features = 5000):
|
|
8
|
+
'''
|
|
9
|
+
Function to calculate approximate kernels widths to inform distribution for project of Fourier Features. Calculates one sigma per group of features
|
|
10
|
+
Input:
|
|
11
|
+
adata- Adata obj as created by `Create_Adata`
|
|
12
|
+
n_features- Number of random features to include when estimating sigma. Will be scaled for the whole pathway set according to a heuristic. Used for scalability
|
|
13
|
+
Output:
|
|
14
|
+
adata object with sigma values. Sigmas accessible by adata.uns['sigma']
|
|
15
|
+
|
|
16
|
+
'''
|
|
17
|
+
|
|
18
|
+
sigma_list = []
|
|
19
|
+
|
|
20
|
+
# Loop over every group in group_dict
|
|
21
|
+
for group_name, group_features in adata.uns['group_dict'].items():
|
|
22
|
+
|
|
23
|
+
# Select only features within that group and downsample for scalability
|
|
24
|
+
num_group_features = len(group_features)
|
|
25
|
+
group_features = adata.uns['seed_obj'].choice(np.array(list(group_features)), min([n_features, num_group_features]), replace = False)
|
|
26
|
+
|
|
27
|
+
X_train = adata[adata.uns['train_indices'], group_features].X
|
|
28
|
+
|
|
29
|
+
X_train = process_data(X_train = X_train, data_type = adata.uns['data_type'], return_dense = True)
|
|
30
|
+
|
|
31
|
+
# Sample cells because distance calculation are costly and can be approximated
|
|
32
|
+
distance_indices = adata.uns['seed_obj'].choice(np.arange(X_train.shape[0]), np.min((2000, X_train.shape[0])))
|
|
33
|
+
|
|
34
|
+
# Calculate Distance Matrix with specified metric
|
|
35
|
+
sigma = np.mean(scipy.spatial.distance.cdist(X_train[distance_indices,:], X_train[distance_indices,:], adata.uns['distance_metric']))
|
|
36
|
+
|
|
37
|
+
if sigma == 0:
|
|
38
|
+
sigma += 1e-5
|
|
39
|
+
|
|
40
|
+
if n_features < num_group_features:
|
|
41
|
+
sigma = sigma * num_group_features / n_features # Heuristic we calculated to account for fewer features used in distance calculation
|
|
42
|
+
|
|
43
|
+
sigma_list.append(sigma)
|
|
44
|
+
|
|
45
|
+
adata.uns['sigma'] = np.array(sigma_list)
|
|
46
|
+
|
|
47
|
+
return adata
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import anndata as ad
|
|
3
|
+
import gc
|
|
4
|
+
|
|
5
|
+
from scmkl.tfidf import tfidf_normalize
|
|
6
|
+
from scmkl.estimate_sigma import estimate_sigma
|
|
7
|
+
from scmkl.approx_kernels import calculate_z
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def combine_modalities(assay_1_name: str, assay_2_name: str,
|
|
11
|
+
assay_1_adata, assay_2_adata,
|
|
12
|
+
combination = 'concatenate'):
|
|
13
|
+
'''
|
|
14
|
+
Combines data sets for multimodal classification. Combined group names are assay+group_name
|
|
15
|
+
Input:
|
|
16
|
+
Assay_#_name: Name of assay to be added to group_names as a string if overlap
|
|
17
|
+
Assay_#_adata: Anndata object containing Z matrices and annotations
|
|
18
|
+
combination: How to combine the matrices, either sum or concatenate
|
|
19
|
+
Output:
|
|
20
|
+
combined_adata: Adata object with the combined Z matrices and annotations. Annotations must match
|
|
21
|
+
'''
|
|
22
|
+
assert assay_1_adata.shape[0] == assay_2_adata.shape[0], 'Cannot combine data with different number of cells.'
|
|
23
|
+
assert assay_1_name != assay_2_name, 'Assay names must be distinct'
|
|
24
|
+
assert combination.lower() in ['sum', 'concatenate']
|
|
25
|
+
|
|
26
|
+
assay1_groups = set(list(assay_1_adata.uns['group_dict'].keys()))
|
|
27
|
+
assay2_groups = set(list(assay_2_adata.uns['group_dict'].keys()))
|
|
28
|
+
|
|
29
|
+
if not np.all(assay_1_adata.uns['train_indices'] == assay_2_adata.uns['train_indices']):
|
|
30
|
+
|
|
31
|
+
assay_1_adata = ad.concat((assay_1_adata[assay_1_adata.uns['train_indices']], assay_1_adata[assay_1_adata.uns['test_indices']]), uns_merge = 'same')
|
|
32
|
+
assay_2_adata = ad.concat((assay_2_adata[assay_2_adata.uns['train_indices']], assay_2_adata[assay_2_adata.uns['test_indices']]), uns_merge = 'same')
|
|
33
|
+
|
|
34
|
+
assay_1_adata.uns['train_indices'] = np.arange(len(assay_1_adata.uns['train_indices']))
|
|
35
|
+
assay_1_adata.uns['test_indices'] = np.arange(len(assay_1_adata.uns['train_indices']),
|
|
36
|
+
len(assay_1_adata.uns['train_indices']) + len(assay_1_adata.uns['test_indices']))
|
|
37
|
+
|
|
38
|
+
assay_2_adata.uns['train_indices'] = assay_1_adata.uns['train_indices'].copy()
|
|
39
|
+
assay_2_adata.uns['test_indices'] = assay_1_adata.uns['test_indices'].copy()
|
|
40
|
+
|
|
41
|
+
combined_adata = ad.concat((assay_1_adata, assay_2_adata), uns_merge = 'same', axis = 1, label = 'labels')
|
|
42
|
+
combined_adata.obs = assay_1_adata.obs
|
|
43
|
+
|
|
44
|
+
if 'Z_train' in assay_1_adata.uns and 'Z_train' in assay_2_adata.uns:
|
|
45
|
+
if combination == 'concatenate':
|
|
46
|
+
combined_adata.uns['Z_train'] = np.hstack((assay_1_adata.uns['Z_train'], assay_2_adata.uns['Z_train']))
|
|
47
|
+
combined_adata.uns['Z_test'] = np.hstack((assay_1_adata.uns['Z_test'], assay_2_adata.uns['Z_test']))
|
|
48
|
+
|
|
49
|
+
elif combination == 'sum':
|
|
50
|
+
assert assay_1_adata.uns['Z_train'].shape == assay_2_adata.uns['Z_train'].shape, 'Cannot sum Z matrices with different dimensions'
|
|
51
|
+
combined_adata.uns['Z_train'] = assay_1_adata.uns['Z_train'] + assay_2_adata.uns['Z_train']
|
|
52
|
+
combined_adata.uns['Z_test'] = assay_1_adata.uns['Z_test'] + assay_2_adata.uns['Z_test']
|
|
53
|
+
|
|
54
|
+
group_dict1 = assay_1_adata.uns['group_dict']
|
|
55
|
+
group_dict2 = assay_2_adata.uns['group_dict']
|
|
56
|
+
|
|
57
|
+
if len(assay1_groups.intersection(assay2_groups)) > 0:
|
|
58
|
+
new_dict = {}
|
|
59
|
+
for group, features in group_dict1.items():
|
|
60
|
+
new_dict[f'{assay_1_name}-{group}'] = features
|
|
61
|
+
|
|
62
|
+
group_dict1 = new_dict
|
|
63
|
+
|
|
64
|
+
new_dict = {}
|
|
65
|
+
for group, features in group_dict2.items():
|
|
66
|
+
new_dict[f'{assay_2_name}-{group}'] = features
|
|
67
|
+
|
|
68
|
+
group_dict2 = new_dict
|
|
69
|
+
|
|
70
|
+
group_dict = group_dict1 | group_dict2 #Combines the dictionaries
|
|
71
|
+
combined_adata.uns['group_dict'] = group_dict
|
|
72
|
+
|
|
73
|
+
if 'seed_obj' in assay_1_adata.uns_keys():
|
|
74
|
+
combined_adata.uns['seed_obj'] = assay_1_adata.uns['seed_obj']
|
|
75
|
+
else:
|
|
76
|
+
print('No random seed present in Adata, it is recommended for reproducibility.')
|
|
77
|
+
|
|
78
|
+
del assay_1_adata, assay_2_adata
|
|
79
|
+
gc.collect()
|
|
80
|
+
|
|
81
|
+
return combined_adata
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def multimodal_processing(assay1: str, assay2: str ,adata1, adata2, tfidf: list, z_calculation = False):
|
|
85
|
+
'''
|
|
86
|
+
Function to remove rows from both modalities when there is no signal present in at least 1.
|
|
87
|
+
|
|
88
|
+
Input:
|
|
89
|
+
assay<N>- Name of assay data contained in adata<N>
|
|
90
|
+
adata<N>- adata object created as above.
|
|
91
|
+
tfidf- list of boolean values whether to tfidf the respective matrices
|
|
92
|
+
z_calculation- Boolean value whether to calculate sigma and Z on the adata before combining
|
|
93
|
+
Allows for individual kernel functions for each
|
|
94
|
+
Output:
|
|
95
|
+
adata- Concatenated adata objects with empty rows removed and matching order
|
|
96
|
+
'''
|
|
97
|
+
|
|
98
|
+
import warnings
|
|
99
|
+
warnings.filterwarnings('ignore')
|
|
100
|
+
|
|
101
|
+
assert adata1.shape[0] == adata2.shape[0], 'Different number of cells present in each object'
|
|
102
|
+
assert np.all(adata1.uns['train_indices'] == adata2.uns['train_indices']), 'Different train indices'
|
|
103
|
+
assert np.all(adata1.uns['test_indices'] == adata2.uns['test_indices']), 'Different test indices'
|
|
104
|
+
|
|
105
|
+
non_empty_rows1 = np.where(np.sum(adata1.X, axis = 1) > 0)[0]
|
|
106
|
+
non_empty_rows2 = np.where(np.sum(adata2.X, axis = 1) > 0)[0]
|
|
107
|
+
|
|
108
|
+
train_test = np.repeat('train', adata1.shape[0])
|
|
109
|
+
train_test[adata1.uns['test_indices']] = 'test'
|
|
110
|
+
|
|
111
|
+
non_empty_rows = np.intersect1d(non_empty_rows1, non_empty_rows2)
|
|
112
|
+
|
|
113
|
+
train_test = train_test[non_empty_rows]
|
|
114
|
+
train_indices = np.where(train_test == 'train')[0]
|
|
115
|
+
test_indices = np.where(train_test == 'test')[0]
|
|
116
|
+
|
|
117
|
+
adata1.uns['train_indices'] = train_indices
|
|
118
|
+
adata2.uns['train_indices'] = train_indices
|
|
119
|
+
adata1.uns['test_indices'] = test_indices
|
|
120
|
+
adata2.uns['test_indices'] = test_indices
|
|
121
|
+
|
|
122
|
+
adata1 = adata1[non_empty_rows, :]
|
|
123
|
+
adata2 = adata2[non_empty_rows, :]
|
|
124
|
+
|
|
125
|
+
if tfidf[0]:
|
|
126
|
+
adata1 = tfidf_normalize(adata1, binarize= True)
|
|
127
|
+
if tfidf[1]:
|
|
128
|
+
adata2 = tfidf_normalize(adata2, binarize= True)
|
|
129
|
+
|
|
130
|
+
if z_calculation:
|
|
131
|
+
print('Estimating Sigma', flush = True)
|
|
132
|
+
adata1 = estimate_sigma(adata1, n_features= 200)
|
|
133
|
+
adata2 = estimate_sigma(adata2, n_features= 200)
|
|
134
|
+
|
|
135
|
+
print('Calculating Z', flush = True)
|
|
136
|
+
adata1 = calculate_z(adata1, n_features = 5000)
|
|
137
|
+
adata2 = calculate_z(adata2, n_features= 5000)
|
|
138
|
+
|
|
139
|
+
if 'labels' in adata1.obs:
|
|
140
|
+
assert np.all(adata1.obs['labels'] == adata2.obs['labels']), 'Cell labels do not match between adata objects'
|
|
141
|
+
|
|
142
|
+
adata = combine_modalities(assay1, assay2, adata1, adata2, 'concatenate')
|
|
143
|
+
|
|
144
|
+
del adata1, adata2
|
|
145
|
+
gc.collect()
|
|
146
|
+
|
|
147
|
+
return adata
|
|
148
|
+
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import gc
|
|
3
|
+
import tracemalloc
|
|
4
|
+
|
|
5
|
+
from scmkl.tfidf import tfidf_normalize
|
|
6
|
+
from scmkl.estimate_sigma import estimate_sigma
|
|
7
|
+
from scmkl.approx_kernels import calculate_z
|
|
8
|
+
from scmkl.train import train_model
|
|
9
|
+
from scmkl.test import calculate_auroc, find_selected_groups
|
|
10
|
+
from scmkl.multimodal_processing import multimodal_processing
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def optimize_alpha(adata, group_size, tfidf = False, alpha_array = np.round(np.linspace(1.9,0.1, 10),2), k = 4):
|
|
14
|
+
'''
|
|
15
|
+
Iteratively train a grouplasso model and update alpha to find the parameter yielding the desired sparsity.
|
|
16
|
+
This function is meant to find a good starting point for your model, and the alpha may need further fine tuning.
|
|
17
|
+
Input:
|
|
18
|
+
adata- Anndata object with Z_train and Z_test calculated
|
|
19
|
+
group_size- Argument describing how the features are grouped.
|
|
20
|
+
From Celer documentation:
|
|
21
|
+
"groupsint | list of ints | list of lists of ints.
|
|
22
|
+
Partition of features used in the penalty on w.
|
|
23
|
+
If an int is passed, groups are contiguous blocks of features, of size groups.
|
|
24
|
+
If a list of ints is passed, groups are assumed to be contiguous, group number g being of size groups[g].
|
|
25
|
+
If a list of lists of ints is passed, groups[g] contains the feature indices of the group number g."
|
|
26
|
+
If 1, model will behave identically to Lasso Regression.
|
|
27
|
+
tfidf- Boolean value to determine if TFIDF normalization should be run at each fold. True means that it will be performed.
|
|
28
|
+
starting_alpha- The alpha value to start the search at.
|
|
29
|
+
alpha_array- Numpy array of all alpha values to be tested
|
|
30
|
+
k- number of folds to perform cross validation over
|
|
31
|
+
|
|
32
|
+
Output:
|
|
33
|
+
sparsity_dict- Dictionary with tested alpha as keys and the number of selected pathways as the values
|
|
34
|
+
alpha- The alpha value yielding the number of selected groups closest to the target.
|
|
35
|
+
'''
|
|
36
|
+
|
|
37
|
+
assert isinstance(k, int) and k > 0, 'Must be a positive integer number of folds'
|
|
38
|
+
|
|
39
|
+
import warnings
|
|
40
|
+
warnings.filterwarnings('ignore')
|
|
41
|
+
|
|
42
|
+
y = adata.obs['labels'].iloc[adata.uns['train_indices']].to_numpy()
|
|
43
|
+
|
|
44
|
+
positive_indices = np.where(y == np.unique(y)[0])[0]
|
|
45
|
+
negative_indices = np.setdiff1d(np.arange(len(y)), positive_indices)
|
|
46
|
+
|
|
47
|
+
positive_annotations = np.arange(len(positive_indices)) % k
|
|
48
|
+
negative_annotations = np.arange(len(negative_indices)) % k
|
|
49
|
+
|
|
50
|
+
auc_array = np.zeros((len(alpha_array), k))
|
|
51
|
+
|
|
52
|
+
gc.collect()
|
|
53
|
+
|
|
54
|
+
for fold in np.arange(k):
|
|
55
|
+
|
|
56
|
+
print(f'Fold {fold + 1}:\n Memory Usage: {[mem / 1e9 for mem in tracemalloc.get_traced_memory()]} GB')
|
|
57
|
+
|
|
58
|
+
cv_adata = adata[adata.uns['train_indices'],:]
|
|
59
|
+
|
|
60
|
+
fold_train = np.concatenate((positive_indices[np.where(positive_annotations != fold)[0]], negative_indices[np.where(negative_annotations != fold)[0]]))
|
|
61
|
+
fold_test = np.concatenate((positive_indices[np.where(positive_annotations == fold)[0]], negative_indices[np.where(negative_annotations == fold)[0]]))
|
|
62
|
+
|
|
63
|
+
cv_adata.uns['train_indices'] = fold_train
|
|
64
|
+
cv_adata.uns['test_indices'] = fold_test
|
|
65
|
+
|
|
66
|
+
if tfidf:
|
|
67
|
+
cv_adata = tfidf_normalize(cv_adata, binarize= True)
|
|
68
|
+
|
|
69
|
+
cv_adata = estimate_sigma(cv_adata, n_features = 200)
|
|
70
|
+
cv_adata = calculate_z(cv_adata, n_features= 5000)
|
|
71
|
+
|
|
72
|
+
gc.collect()
|
|
73
|
+
|
|
74
|
+
for i, alpha in enumerate(alpha_array):
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# print(f' 1. Memory Usage: {[mem / 1e9 for mem in tracemalloc.get_traced_memory()]} GB')
|
|
78
|
+
# print(f' Adata size: {sys.getsizeof(cv_adata) / 1e9}')
|
|
79
|
+
|
|
80
|
+
cv_adata = train_model(cv_adata, group_size, alpha = alpha)
|
|
81
|
+
|
|
82
|
+
# print(f' 2. Memory Usage: {[mem / 1e9 for mem in tracemalloc.get_traced_memory()]} GB')
|
|
83
|
+
# print(f' Adata size: {sys.getsizeof(cv_adata) / 1e9}')
|
|
84
|
+
|
|
85
|
+
auc_array[i, fold] = calculate_auroc(cv_adata)
|
|
86
|
+
|
|
87
|
+
# print(f' 3. Memory Usage: {[mem / 1e9 for mem in tracemalloc.get_traced_memory()]} GB')
|
|
88
|
+
# print(f' Adata size: {sys.getsizeof(cv_adata) / 1e9}')
|
|
89
|
+
gc.collect()
|
|
90
|
+
|
|
91
|
+
del cv_adata
|
|
92
|
+
gc.collect()
|
|
93
|
+
# Take AUROC mean across the k folds
|
|
94
|
+
alpha_star = alpha_array[np.argmax(np.mean(auc_array, axis = 1))]
|
|
95
|
+
gc.collect()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
return alpha_star
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def optimize_sparsity(adata, group_size, starting_alpha = 1.9, increment = 0.2, target = 1, n_iter = 10):
|
|
102
|
+
'''
|
|
103
|
+
Iteratively train a grouplasso model and update alpha to find the parameter yielding the desired sparsity.
|
|
104
|
+
This function is meant to find a good starting point for your model, and the alpha may need further fine tuning.
|
|
105
|
+
Input:
|
|
106
|
+
adata- Anndata object with Z_train and Z_test calculated
|
|
107
|
+
group_size- Argument describing how the features are grouped.
|
|
108
|
+
From Celer documentation:
|
|
109
|
+
"groupsint | list of ints | list of lists of ints.
|
|
110
|
+
Partition of features used in the penalty on w.
|
|
111
|
+
If an int is passed, groups are contiguous blocks of features, of size groups.
|
|
112
|
+
If a list of ints is passed, groups are assumed to be contiguous, group number g being of size groups[g].
|
|
113
|
+
If a list of lists of ints is passed, groups[g] contains the feature indices of the group number g."
|
|
114
|
+
If 1, model will behave identically to Lasso Regression.
|
|
115
|
+
starting_alpha- The alpha value to start the search at.
|
|
116
|
+
increment- amount to adjust alpha by between iterations
|
|
117
|
+
target- The desired number of groups selected by the model.
|
|
118
|
+
n_iter- The maximum number of iterations to run
|
|
119
|
+
|
|
120
|
+
Output:
|
|
121
|
+
sparsity_dict- Dictionary with tested alpha as keys and the number of selected pathways as the values
|
|
122
|
+
alpha- The alpha value yielding the number of selected groups closest to the target.
|
|
123
|
+
'''
|
|
124
|
+
assert increment > 0 and increment < starting_alpha, 'Choose a positive increment less than alpha'
|
|
125
|
+
assert target > 0 and isinstance(target, int), 'Choose an integer target number of groups that is greater than 0'
|
|
126
|
+
assert n_iter > 0 and isinstance(n_iter, int), 'Choose an integer number of iterations that is greater than 0'
|
|
127
|
+
|
|
128
|
+
y_train = adata.obs['labels'].iloc[adata.uns['train_indices']].to_numpy()
|
|
129
|
+
X_train = adata.uns['Z_train']
|
|
130
|
+
|
|
131
|
+
if isinstance(group_size, int):
|
|
132
|
+
num_groups = int(X_train.shape[1]/group_size)
|
|
133
|
+
else:
|
|
134
|
+
num_groups = len(group_size)
|
|
135
|
+
|
|
136
|
+
sparsity_dict = {}
|
|
137
|
+
alpha = starting_alpha
|
|
138
|
+
|
|
139
|
+
for i in np.arange(n_iter):
|
|
140
|
+
model = train_model(adata, group_size, alpha)
|
|
141
|
+
num_selected = len(find_selected_groups(adata))
|
|
142
|
+
|
|
143
|
+
sparsity_dict[np.round(alpha,4)] = num_selected
|
|
144
|
+
|
|
145
|
+
if num_selected < target:
|
|
146
|
+
#Decreasing alpha will increase the number of selected pathways
|
|
147
|
+
if alpha - increment in sparsity_dict.keys():
|
|
148
|
+
# Make increment smaller so the model can't go back and forth between alpha values
|
|
149
|
+
increment /= 2
|
|
150
|
+
alpha = np.max([alpha - increment, 1e-1]) #Ensures that alpha will never be negative
|
|
151
|
+
elif num_selected > target:
|
|
152
|
+
if alpha + increment in sparsity_dict.keys():
|
|
153
|
+
increment /= 2
|
|
154
|
+
alpha += increment
|
|
155
|
+
elif num_selected == target:
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
optimal_alpha = list(sparsity_dict.keys())[np.argmin([np.abs(selected - target) for selected in sparsity_dict.values()])]
|
|
159
|
+
return sparsity_dict, optimal_alpha
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def multimodal_optimize_alpha(adata1, adata2, group_size = 1, tfidf_list = [False, False], alpha_array = np.round(np.linspace(1.9,0.1, 10),2), k = 4):
|
|
163
|
+
'''
|
|
164
|
+
Iteratively train a grouplasso model and update alpha to find the parameter yielding the desired sparsity.
|
|
165
|
+
This function is meant to find a good starting point for your model, and the alpha may need further fine tuning.
|
|
166
|
+
Input:
|
|
167
|
+
adataX- Anndata objects with Z_train and Z_test calculated
|
|
168
|
+
group_size- Argument describing how the features are grouped.
|
|
169
|
+
From Celer documentation:
|
|
170
|
+
"groupsint | list of ints | list of lists of ints.
|
|
171
|
+
Partition of features used in the penalty on w.
|
|
172
|
+
If an int is passed, groups are contiguous blocks of features, of size groups.
|
|
173
|
+
If a list of ints is passed, groups are assumed to be contiguous, group number g being of size groups[g].
|
|
174
|
+
If a list of lists of ints is passed, groups[g] contains the feature indices of the group number g."
|
|
175
|
+
If 1, model will behave identically to Lasso Regression.
|
|
176
|
+
tifidf_list- a boolean mask where tfidf_list[0] and tfidf_list[1] are respective to adata1 and adata2
|
|
177
|
+
If True, tfidf normalization will be applied to the respective adata during cross validation
|
|
178
|
+
starting_alpha- The alpha value to start the search at.
|
|
179
|
+
alpha_array- Numpy array of all alpha values to be tested
|
|
180
|
+
k- number of folds to perform cross validation over
|
|
181
|
+
|
|
182
|
+
Output:
|
|
183
|
+
sparsity_dict- Dictionary with tested alpha as keys and the number of selected pathways as the values
|
|
184
|
+
alpha- The alpha value yielding the number of selected groups closest to the target.
|
|
185
|
+
'''
|
|
186
|
+
|
|
187
|
+
assert isinstance(k, int) and k > 0, 'Must be a positive integer number of folds'
|
|
188
|
+
|
|
189
|
+
import warnings
|
|
190
|
+
warnings.filterwarnings('ignore')
|
|
191
|
+
|
|
192
|
+
y = adata1.obs['labels'].iloc[adata1.uns['train_indices']].to_numpy()
|
|
193
|
+
|
|
194
|
+
positive_indices = np.where(y == np.unique(y)[0])[0]
|
|
195
|
+
negative_indices = np.setdiff1d(np.arange(len(y)), positive_indices)
|
|
196
|
+
|
|
197
|
+
positive_annotations = np.arange(len(positive_indices)) % k
|
|
198
|
+
negative_annotations = np.arange(len(negative_indices)) % k
|
|
199
|
+
|
|
200
|
+
auc_array = np.zeros((len(alpha_array), k))
|
|
201
|
+
|
|
202
|
+
cv_adata1 = adata1[adata1.uns['train_indices'],:]
|
|
203
|
+
cv_adata2 = adata2[adata2.uns['train_indices'],:]
|
|
204
|
+
|
|
205
|
+
del adata1, adata2
|
|
206
|
+
gc.collect()
|
|
207
|
+
|
|
208
|
+
for fold in np.arange(k):
|
|
209
|
+
|
|
210
|
+
print(f'Fold {fold + 1}:\n Memory Usage: {[mem / 1e9 for mem in tracemalloc.get_traced_memory()]} GB')
|
|
211
|
+
|
|
212
|
+
fold_train = np.concatenate((positive_indices[np.where(positive_annotations != fold)[0]], negative_indices[np.where(negative_annotations != fold)[0]]))
|
|
213
|
+
fold_test = np.concatenate((positive_indices[np.where(positive_annotations == fold)[0]], negative_indices[np.where(negative_annotations == fold)[0]]))
|
|
214
|
+
|
|
215
|
+
cv_adata1.uns['train_indices'] = fold_train
|
|
216
|
+
cv_adata1.uns['test_indices'] = fold_test
|
|
217
|
+
cv_adata2.uns['train_indices'] = fold_train
|
|
218
|
+
cv_adata2.uns['test_indices'] = fold_test
|
|
219
|
+
|
|
220
|
+
cv_adata = multimodal_processing('assay1', 'assay2', cv_adata1, cv_adata2, tfidf_list, z_calculation = True)
|
|
221
|
+
cv_adata.uns['seed_obj'] = cv_adata1.uns['seed_obj']
|
|
222
|
+
|
|
223
|
+
gc.collect()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
for i, alpha in enumerate(alpha_array):
|
|
228
|
+
|
|
229
|
+
cv_adata = train_model(cv_adata, group_size, alpha = alpha)
|
|
230
|
+
|
|
231
|
+
auc_array[i, fold] = calculate_auroc(cv_adata)
|
|
232
|
+
|
|
233
|
+
# gc.collect()
|
|
234
|
+
del cv_adata
|
|
235
|
+
gc.collect()
|
|
236
|
+
# Take AUROC mean across the k folds
|
|
237
|
+
alpha_star = alpha_array[np.argmax(np.mean(auc_array, axis = 1))]
|
|
238
|
+
del cv_adata1, cv_adata2
|
|
239
|
+
gc.collect()
|
|
240
|
+
|
|
241
|
+
return alpha_star
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import sklearn
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def predict(adata, metrics = None):
|
|
7
|
+
'''
|
|
8
|
+
Function to return predicted labels and calculate any of AUROC, Accuracy, F1 Score, Precision, Recall for a classification.
|
|
9
|
+
Input:
|
|
10
|
+
adata- adata object with trained model and Z matrices in uns
|
|
11
|
+
metrics- Which metrics to calculate on the predicted values
|
|
12
|
+
|
|
13
|
+
Output:
|
|
14
|
+
Values predicted by the model
|
|
15
|
+
Dictionary containing AUROC, Accuracy, F1 Score, Precision, and/or Recall depending on metrics argument
|
|
16
|
+
|
|
17
|
+
'''
|
|
18
|
+
y_test = adata.obs['labels'].iloc[adata.uns['test_indices']].to_numpy()
|
|
19
|
+
X_test = adata.uns['Z_test']
|
|
20
|
+
assert X_test.shape[0] == len(y_test), 'X and y must have the same number of samples'
|
|
21
|
+
assert all([metric in ['AUROC', 'Accuracy', 'F1-Score', 'Precision', 'Recall'] for metric in metrics]), 'Unknown metric provided. Must be one or more of AUROC, Accuracy, F1-Score, Precision, Recall'
|
|
22
|
+
|
|
23
|
+
# Sigmoid function to force probabilities into [0,1]
|
|
24
|
+
probabilities = 1 / (1 + np.exp(-adata.uns['model'].predict(X_test)))
|
|
25
|
+
|
|
26
|
+
# Group Lasso requires 'continous' y values need to re-descritize it
|
|
27
|
+
y = np.zeros((len(y_test)))
|
|
28
|
+
y[y_test == np.unique(y_test)[0]] = 1
|
|
29
|
+
|
|
30
|
+
metric_dict = {}
|
|
31
|
+
|
|
32
|
+
#Convert numerical probabilities into binary phenotype
|
|
33
|
+
y_pred = np.array(np.repeat(np.unique(y_test)[1], len(y_test)), dtype = 'object')
|
|
34
|
+
y_pred[np.round(probabilities,0).astype(int) == 1] = np.unique(y_test)[0]
|
|
35
|
+
|
|
36
|
+
if metrics == None:
|
|
37
|
+
return y_pred
|
|
38
|
+
|
|
39
|
+
if 'AUROC' in metrics:
|
|
40
|
+
fpr, tpr, _ = sklearn.metrics.roc_curve(y, probabilities)
|
|
41
|
+
metric_dict['AUROC'] = sklearn.metrics.auc(fpr, tpr)
|
|
42
|
+
if 'Accuracy' in metrics:
|
|
43
|
+
metric_dict['Accuracy'] = np.mean(y_test == y_pred)
|
|
44
|
+
if 'F1-Score' in metrics:
|
|
45
|
+
metric_dict['F1-Score'] = sklearn.metrics.f1_score(y_test, y_pred, pos_label = np.unique(y_test)[0])
|
|
46
|
+
if 'Precision' in metrics:
|
|
47
|
+
metric_dict['Precision'] = sklearn.metrics.precision_score(y_test, y_pred, pos_label = np.unique(y_test)[0])
|
|
48
|
+
if 'Recall' in metrics:
|
|
49
|
+
metric_dict['Recall'] = sklearn.metrics.recall_score(y_test, y_pred, pos_label = np.unique(y_test)[0])
|
|
50
|
+
|
|
51
|
+
return y_pred, metric_dict
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_selected_groups(adata) -> np.ndarray:
|
|
55
|
+
|
|
56
|
+
'''
|
|
57
|
+
Function to find feature groups selected by the model during training. If feature weight assigned by the model is non-0, then the group containing that feature is selected.
|
|
58
|
+
Inputs:
|
|
59
|
+
model- A trained celer.GroupLasso model.
|
|
60
|
+
group_names- An iterable object containing the group_names in the same order as the feature groupings from Data array.
|
|
61
|
+
Outpus:
|
|
62
|
+
Numpy array containing the names of the groups selected by the model.
|
|
63
|
+
'''
|
|
64
|
+
|
|
65
|
+
selected_groups = []
|
|
66
|
+
coefficients = adata.uns['model'].coef_
|
|
67
|
+
group_size = adata.uns['model'].get_params()['groups']
|
|
68
|
+
group_names = np.array(list(adata.uns['group_dict'].keys()))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
for i, group in enumerate(group_names):
|
|
72
|
+
if not isinstance(group_size, (list, set, np.ndarray, tuple)):
|
|
73
|
+
group_norm = np.linalg.norm(coefficients[np.arange(i * group_size, (i+1) * group_size - 1)])
|
|
74
|
+
else:
|
|
75
|
+
group_norm = np.linalg.norm(coefficients[group_size[i]])
|
|
76
|
+
|
|
77
|
+
if group_norm != 0:
|
|
78
|
+
selected_groups.append(group)
|
|
79
|
+
|
|
80
|
+
return np.array(selected_groups)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def calculate_auroc(adata)-> float:
|
|
84
|
+
'''
|
|
85
|
+
Function to calculate the AUROC for a classification.
|
|
86
|
+
Designed as a helper function. Recommended to use Predict() for model evaluation.
|
|
87
|
+
Input:
|
|
88
|
+
adata- adata object with trained model and Z matrices in uns
|
|
89
|
+
Output:
|
|
90
|
+
Calculated AUROC value
|
|
91
|
+
'''
|
|
92
|
+
|
|
93
|
+
y_test = adata.obs['labels'].iloc[adata.uns['test_indices']].to_numpy()
|
|
94
|
+
X_test = adata.uns['Z_test']
|
|
95
|
+
|
|
96
|
+
y_test = y_test.ravel()
|
|
97
|
+
assert X_test.shape[0] == len(y_test), f'X has {X_test.shape[0]} samples and y has {len(y_test)} samples.'
|
|
98
|
+
|
|
99
|
+
# Sigmoid function to force probabilities into [0,1]
|
|
100
|
+
probabilities = 1 / (1 + np.exp(-adata.uns['model'].predict(X_test)))
|
|
101
|
+
# Group Lasso requires 'continous' y values need to re-descritize it
|
|
102
|
+
|
|
103
|
+
y = np.zeros((len(y_test)))
|
|
104
|
+
y[y_test == np.unique(y_test)[0]] = 1
|
|
105
|
+
fpr, tpr, _ = sklearn.metrics.roc_curve(y, probabilities)
|
|
106
|
+
auc = sklearn.metrics.auc(fpr, tpr)
|
|
107
|
+
|
|
108
|
+
return(auc)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def extract_kernel_weights():
|
|
112
|
+
'''
|
|
113
|
+
'''
|
|
114
|
+
pass
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def tfidf_filter(X, mode = 'filter'):
|
|
6
|
+
'''
|
|
7
|
+
Function to use Term Frequency Inverse Document Frequency filtering for atac data to find meaningful features.
|
|
8
|
+
If input is pandas data frame or scipy sparse array, it will be converted to a numpy array.
|
|
9
|
+
Input:
|
|
10
|
+
x- Data matrix of cell x feature. Must be a Numpy array or Scipy sparse array.
|
|
11
|
+
mode- Argument to determine what to return. Must be filter or normalize
|
|
12
|
+
Output:
|
|
13
|
+
TFIDF- Output depends on given 'mode' parameter
|
|
14
|
+
'filter'- returns which column sums are non 0 i.e. which features are significant
|
|
15
|
+
'normalize'- returns TFIDF filtered data matrix of the same dimensions as x. Returns as scipy sparse matrix
|
|
16
|
+
'''
|
|
17
|
+
|
|
18
|
+
assert mode in ['filter', 'normalize'], 'mode must be "filter" or "normalize".'
|
|
19
|
+
|
|
20
|
+
if scipy.sparse.issparse(X):
|
|
21
|
+
# row_sum = np.array(X.sum(axis=1)).flatten()
|
|
22
|
+
tf = scipy.sparse.csc_array(X)# / row_sum[:, np.newaxis])
|
|
23
|
+
doc_freq = np.array(np.sum(X > 0, axis=0)).flatten()
|
|
24
|
+
else:
|
|
25
|
+
# row_sum = np.sum(X, axis=1, keepdims=True)
|
|
26
|
+
tf = X# / row_sum
|
|
27
|
+
doc_freq = np.sum(X > 0, axis=0)
|
|
28
|
+
|
|
29
|
+
idf = np.log1p((1 + X.shape[0]) / (1 + doc_freq))
|
|
30
|
+
tfidf = tf * idf
|
|
31
|
+
|
|
32
|
+
if mode == 'normalize':
|
|
33
|
+
if scipy.sparse.issparse(tfidf):
|
|
34
|
+
tfidf = scipy.sparse.csc_matrix(tfidf)
|
|
35
|
+
return tfidf
|
|
36
|
+
elif mode == 'filter':
|
|
37
|
+
significant_features = np.where(np.sum(tfidf, axis=0) > 0)[0]
|
|
38
|
+
return significant_features
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def tfidf_normalize(adata, binarize = False):
|
|
42
|
+
|
|
43
|
+
'''
|
|
44
|
+
Function to TF IDF normalize the data in an adata object
|
|
45
|
+
If train/test indices are included in the object, it will calculate the normalization separately for the training and testing data
|
|
46
|
+
Otherwise it will calculate it on the entire dataset
|
|
47
|
+
If any rows are entirely 0, that row and its metadata will be removed from the object
|
|
48
|
+
|
|
49
|
+
Input:
|
|
50
|
+
adata- adata object with data in adata.X to be normalized
|
|
51
|
+
Can have train/test indices included or not
|
|
52
|
+
binarize- Boolean option to binarize the data
|
|
53
|
+
Output:
|
|
54
|
+
adata- adata object with same attributes as before, but the TF IDF normalized matrix in place of adata.X
|
|
55
|
+
Will now have the train data stacked on test data, and the indices will be adjusted accordingly
|
|
56
|
+
'''
|
|
57
|
+
|
|
58
|
+
X = adata.X.copy()
|
|
59
|
+
row_sums = np.sum(X, axis = 1)
|
|
60
|
+
assert np.all(row_sums > 0), 'TFIDF requires all row sums be positive'
|
|
61
|
+
|
|
62
|
+
if binarize:
|
|
63
|
+
X[X>0] = 1
|
|
64
|
+
|
|
65
|
+
if 'train_indices' in adata.uns_keys():
|
|
66
|
+
|
|
67
|
+
train_indices = adata.uns['train_indices'].copy()
|
|
68
|
+
test_indices = adata.uns['test_indices'].copy()
|
|
69
|
+
|
|
70
|
+
tfidf_train = tfidf_filter(X[train_indices,:], mode = 'normalize')
|
|
71
|
+
tfidf_test = tfidf_filter(X, mode = 'normalize')[test_indices,:]
|
|
72
|
+
|
|
73
|
+
if scipy.sparse.issparse(X):
|
|
74
|
+
tfidf_norm = scipy.sparse.vstack((tfidf_train, tfidf_test))
|
|
75
|
+
else:
|
|
76
|
+
tfidf_norm = np.vstack((tfidf_train, tfidf_test))
|
|
77
|
+
|
|
78
|
+
## I'm not sure why this reassignment is necessary, but without, the values will be saved as 0s in adata
|
|
79
|
+
adata.uns['train_indices'] = train_indices
|
|
80
|
+
adata.uns['test_indices'] = test_indices
|
|
81
|
+
|
|
82
|
+
combined_indices = np.concatenate((train_indices, test_indices))
|
|
83
|
+
|
|
84
|
+
adata_index = adata.obs_names[combined_indices].astype(int)
|
|
85
|
+
tfidf_norm = tfidf_norm[np.argsort(adata_index),:]
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
|
|
89
|
+
tfidf_norm = tfidf_filter(X, mode = 'normalize')
|
|
90
|
+
|
|
91
|
+
adata.X = tfidf_norm.copy()
|
|
92
|
+
# adata = adata[adata.obs_names,:]
|
|
93
|
+
|
|
94
|
+
return adata
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import celer
|
|
3
|
+
import gc
|
|
4
|
+
import tracemalloc
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def train_model(adata, group_size = 1, alpha = 0.9):
|
|
8
|
+
'''
|
|
9
|
+
Function to fit a grouplasso model to the provided data.
|
|
10
|
+
Inputs:
|
|
11
|
+
Adata with Z matrices in adata.uns
|
|
12
|
+
group_size- Argument describing how the features are grouped.
|
|
13
|
+
From Celer documentation:
|
|
14
|
+
"groupsint | list of ints | list of lists of ints.
|
|
15
|
+
Partition of features used in the penalty on w.
|
|
16
|
+
If an int is passed, groups are contiguous blocks of features, of size groups.
|
|
17
|
+
If a list of ints is passed, groups are assumed to be contiguous, group number g being of size groups[g].
|
|
18
|
+
If a list of lists of ints is passed, groups[g] contains the feature indices of the group number g."
|
|
19
|
+
If 1, model will behave identically to Lasso Regression
|
|
20
|
+
alpha- Group Lasso regularization coefficient. alpha is a floating point value controlling model solution sparsity. Must be a positive float.
|
|
21
|
+
The smaller the value, the more feature groups will be selected in the trained model.
|
|
22
|
+
Outputs:
|
|
23
|
+
|
|
24
|
+
adata object with trained model in uns accessible with- adata.uns['model']
|
|
25
|
+
Specifics of model:
|
|
26
|
+
model- The trained Celer Group Lasso model. Used in Find_Selected_Pathways() function for interpretability.
|
|
27
|
+
For more information about attributes and methods see the Celer documentation at https://mathurinm.github.io/celer/generated/celer.GroupLasso.html.
|
|
28
|
+
|
|
29
|
+
'''
|
|
30
|
+
assert alpha > 0, 'Alpha must be positive'
|
|
31
|
+
|
|
32
|
+
y_train = adata.obs['labels'].iloc[adata.uns['train_indices']]
|
|
33
|
+
X_train = adata.uns['Z_train']
|
|
34
|
+
|
|
35
|
+
cell_labels = np.unique(y_train)
|
|
36
|
+
|
|
37
|
+
# This is a regression algorithm. We need to make the labels 'continuous' for classification, but they will remain binary.
|
|
38
|
+
# Casts training labels to array of -1,1
|
|
39
|
+
train_labels = np.ones(y_train.shape)
|
|
40
|
+
train_labels[y_train == cell_labels[1]] = -1
|
|
41
|
+
|
|
42
|
+
# Alphamax is a calculation to regularize the effect of alpha (a sparsity parameter) across different data sets
|
|
43
|
+
alphamax = np.max(np.abs(X_train.T.dot(train_labels))) / X_train.shape[0] * alpha
|
|
44
|
+
|
|
45
|
+
# Instantiate celer Group Lasso Regression Model Object
|
|
46
|
+
model = celer.GroupLasso(groups = group_size, alpha = alphamax)
|
|
47
|
+
|
|
48
|
+
# Fit model using training data
|
|
49
|
+
model.fit(X_train, train_labels.ravel())
|
|
50
|
+
|
|
51
|
+
adata.uns['model'] = model
|
|
52
|
+
return adata
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def find_overlap(start1, end1, start2, end2) -> bool:
|
|
6
|
+
'''
|
|
7
|
+
Function to determine whether two regions on the same chromosome overlap.
|
|
8
|
+
Input:
|
|
9
|
+
start1 : the start position for region 1
|
|
10
|
+
end1 : the end position for region 1
|
|
11
|
+
start2 : the start position for region 2
|
|
12
|
+
end2: the end postion for region 2
|
|
13
|
+
Output:
|
|
14
|
+
True if the regions overlap by 1bp
|
|
15
|
+
False if the regions do not overlap
|
|
16
|
+
'''
|
|
17
|
+
return max(start1,start2) <= min(end1, end2)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_atac_groupings(gene_library : dict, feature_names : list | np.ndarray | pd.Series, gene_annotations : pd.DataFrame) -> dict:
|
|
21
|
+
'''
|
|
22
|
+
Function to create an ATAC region grouping for scMKL using genes in a gene set library.
|
|
23
|
+
Searches for regions in gene_annotations that overlap with assay features, then matches gene_names to genes in gene_library to create grouping.
|
|
24
|
+
Input:
|
|
25
|
+
gene_library : a dictionary with gene set names as keys and a set | list | np.ndarray of gene names
|
|
26
|
+
feature_names : an array of feature regions in scATAC assay
|
|
27
|
+
gene_annotations : a pd.DataFrame with columns [chr, start, stop, gene_name] where [chr, start, stop] for each row is the region of the gene_name gene body
|
|
28
|
+
Output:
|
|
29
|
+
ATAC_group_dict : a grouping dictionary with gene set names from gene_library as keys and an array of regions as values.
|
|
30
|
+
'''
|
|
31
|
+
assert ('chr' and 'start' and 'end' and 'gene_name') in gene_annotations.columns, "gene_annotations argument must be a dataframe with columns ['chr', 'start', 'end', 'gene_name']"
|
|
32
|
+
|
|
33
|
+
# Variables for region comparison and grouping creation
|
|
34
|
+
peak_gene_dict = {}
|
|
35
|
+
ga_regions = {}
|
|
36
|
+
feature_dict = {}
|
|
37
|
+
ATAC_grouping = {group : [] for group in gene_library.keys()}
|
|
38
|
+
|
|
39
|
+
# Creating a list of all gene names to filter gene annotations by, ensuring there are no NaN values in list
|
|
40
|
+
all_genes = [gene for group in gene_library.keys() for gene in gene_library[group] if type(gene) != float]
|
|
41
|
+
|
|
42
|
+
# Filtering gene_annotations by genes in the gene_library
|
|
43
|
+
gene_annotations = gene_annotations[np.isin(gene_annotations['gene_name'], all_genes)]
|
|
44
|
+
|
|
45
|
+
# Creating dictionaries from gene_annotations where:
|
|
46
|
+
# peak_gene_dict - (chr, start_location, end_location) : gene_name
|
|
47
|
+
# ga_regions - chr : np.ndarray([[start_location, end_location], [start_location, end_location], ...])
|
|
48
|
+
for i, anno in gene_annotations.iterrows():
|
|
49
|
+
peak_gene_dict[(anno['chr'], int(anno['start']), int(anno['end']))] = anno['gene_name']
|
|
50
|
+
if anno['chr'] in ga_regions.keys():
|
|
51
|
+
ga_regions[anno['chr']] = np.concatenate((ga_regions[anno['chr']], np.array([[anno['start'], anno['end']]], dtype = int)), axis = 0)
|
|
52
|
+
else:
|
|
53
|
+
ga_regions[anno['chr']] = np.array([[anno['start'], anno['end']]], dtype=int)
|
|
54
|
+
|
|
55
|
+
print("Gene Annotations Formatted", flush = True)
|
|
56
|
+
|
|
57
|
+
# Reformatting feature names to a list of lists where each element is a list of [chr, start_location, stop_location]
|
|
58
|
+
feature_names = [peak.split("-") for peak in feature_names]
|
|
59
|
+
# Creating a dictionary of features from assay where chr : np.ndarray([[start_location, end_location], [start_location, end_location], ...])
|
|
60
|
+
for peak_set in feature_names:
|
|
61
|
+
if peak_set[0] in feature_dict.keys():
|
|
62
|
+
feature_dict[peak_set[0]] = np.concatenate((feature_dict[peak_set[0]], np.array([[peak_set[1], peak_set[2]]], dtype = int)), axis = 0)
|
|
63
|
+
else:
|
|
64
|
+
feature_dict[peak_set[0]] = np.array([[peak_set[1], peak_set[2]]], dtype = int)
|
|
65
|
+
|
|
66
|
+
print("Assay Peaks Formatted", flush = True)
|
|
67
|
+
|
|
68
|
+
# This is where the regions in the assay and the regions in the annotations are compared then genes are matched between the gene_library and gene_annotation for the respective regions
|
|
69
|
+
# Iterating through all the chromosomes in the feature assay
|
|
70
|
+
print("Comparing Regions", flush = True)
|
|
71
|
+
for chrom in feature_dict.keys():
|
|
72
|
+
# Continuing if chromosom for iteration not in gene_annotations to reduce number of comparisons
|
|
73
|
+
if chrom not in ga_regions.keys():
|
|
74
|
+
continue
|
|
75
|
+
# Iterating through peaks in features for the given chromosome
|
|
76
|
+
for region in feature_dict[chrom]:
|
|
77
|
+
# Iteration through peaks in ga_regions (from gene_annotations) for the current chromosome during the iteration
|
|
78
|
+
for anno in ga_regions[chrom]:
|
|
79
|
+
# Checking if the current feature peak and ga_region peak overlap
|
|
80
|
+
if find_overlap(region[0], region[1], anno[0], anno[1]):
|
|
81
|
+
gene = peak_gene_dict[(chrom, anno[0], anno[1])]
|
|
82
|
+
# Iterating through all of the gene sets in gene_library to match gene for current ga_annotation peak to genes in gene sets
|
|
83
|
+
for group in gene_library.keys():
|
|
84
|
+
if gene in gene_library[group]:
|
|
85
|
+
# Adding feature region to group in ATAC_grouping dict
|
|
86
|
+
ATAC_grouping[group].append("-".join([chrom, str(region[0]), str(region[1])]))
|
|
87
|
+
|
|
88
|
+
print(f'{chrom} Comparisons Complete', flush = True)
|
|
89
|
+
|
|
90
|
+
# Returning a dictionary with keys from gene_library keys and values are arrays of peaks from feature array that overlap with gene peaks from gene_annotations if respective genes are in gene_library[gene_set]
|
|
91
|
+
return ATAC_grouping
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def create_results_df():
|
|
95
|
+
'''
|
|
96
|
+
'''
|
|
97
|
+
pass
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: scmkl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Author: Sam Kupp, Ian VanGordon, Cigdem Ak
|
|
5
|
+
Author-email: kupp@ohsu.edu, vangordi@ohsu.edu, ak@ohsu.edu
|
|
6
|
+
Requires-Python: >3.11.1
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: wheel==0.41.2
|
|
9
|
+
Requires-Dist: anndata==0.10.8
|
|
10
|
+
Requires-Dist: celer==0.7.3
|
|
11
|
+
Requires-Dist: numpy==1.26.0
|
|
12
|
+
Requires-Dist: pandas==2.2.2
|
|
13
|
+
Requires-Dist: scikit-learn==1.3.2
|
|
14
|
+
Requires-Dist: scipy==1.14.1
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
scmkl/__init__.py
|
|
5
|
+
scmkl/approx_kernels.py
|
|
6
|
+
scmkl/data_processing.py
|
|
7
|
+
scmkl/estimate_sigma.py
|
|
8
|
+
scmkl/multimodal_processing.py
|
|
9
|
+
scmkl/optimize_alpha.py
|
|
10
|
+
scmkl/test.py
|
|
11
|
+
scmkl/tfidf.py
|
|
12
|
+
scmkl/train.py
|
|
13
|
+
scmkl/utils.py
|
|
14
|
+
scmkl.egg-info/PKG-INFO
|
|
15
|
+
scmkl.egg-info/SOURCES.txt
|
|
16
|
+
scmkl.egg-info/dependency_links.txt
|
|
17
|
+
scmkl.egg-info/requires.txt
|
|
18
|
+
scmkl.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
scmkl
|
scmkl-0.1.0/setup.cfg
ADDED
scmkl-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='scmkl',
|
|
5
|
+
version='0.1.0',
|
|
6
|
+
author = 'Sam Kupp, Ian VanGordon, Cigdem Ak',
|
|
7
|
+
author_email = 'kupp@ohsu.edu, vangordi@ohsu.edu, ak@ohsu.edu',
|
|
8
|
+
packages=find_packages(),
|
|
9
|
+
python_requires='>3.11.1',
|
|
10
|
+
install_requires = [
|
|
11
|
+
'wheel==0.41.2',
|
|
12
|
+
'anndata==0.10.8',
|
|
13
|
+
'celer==0.7.3',
|
|
14
|
+
'numpy==1.26.0',
|
|
15
|
+
'pandas==2.2.2',
|
|
16
|
+
'scikit-learn==1.3.2',
|
|
17
|
+
'scipy==1.14.1'
|
|
18
|
+
]
|
|
19
|
+
)
|