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 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,7 @@
1
+ wheel==0.41.2
2
+ anndata==0.10.8
3
+ celer==0.7.3
4
+ numpy==1.26.0
5
+ pandas==2.2.2
6
+ scikit-learn==1.3.2
7
+ scipy==1.14.1
@@ -0,0 +1 @@
1
+ scmkl
scmkl-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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
+ )