scmkl.create_adata

  1import numpy as np
  2import anndata as ad
  3import scipy
  4import pandas as pd
  5import gc
  6import warnings
  7
  8from scmkl.data_processing import sparse_var
  9
 10
 11def filter_features(feature_names: np.ndarray, group_dict: dict,
 12                    add_ones: bool):
 13    """
 14    Function to remove features only in feature names or group_dict.
 15    Any features not included in group_dict will be removed from the
 16    matrix. Also puts the features in the same relative order (of 
 17    included features)
 18    
 19    Parameters
 20    ----------
 21    feature_names : np.ndarray
 22        Numpy array of corresponding feature names.
 23
 24    group_dict : dict
 25        Dictionary containing feature grouping information.
 26                 Example: {geneset: np.array(gene_1, gene_2, ..., 
 27                 gene_n)}
 28    Returns
 29    -------
 30    feature_names : np.ndarray
 31        Numpy array of corresponding feature names from group_dict.
 32
 33    group_dict : dict
 34        Dictionary containing features overlapping input grouping 
 35        information and full feature names.
 36    """
 37    group_features = set()
 38    feature_set = set(feature_names)
 39
 40    # Store all objects in dictionary in set
 41    for group in group_dict.keys():
 42        group_features.update(set(group_dict[group]))
 43
 44        # Finds intersection between group features and features in data
 45        # Converts to nd.array and sorts to preserve order of feature names
 46        group_feats = list(feature_set.intersection(set(group_dict[group])))
 47        group_dict[group] = np.sort(np.array(group_feats))
 48
 49    # Only keeping groupings that have at least two features
 50    min_features = 1 if add_ones else 2
 51    group_dict = {group : group_dict[group] for group in group_dict.keys()
 52                  if len(group_dict[group]) >= min_features}
 53
 54    group_features = np.array(list(group_features.intersection(feature_set)))
 55
 56    return group_features, group_dict
 57
 58
 59def multi_class_split(y: np.ndarray, seed_obj: np.random._generator.Generator, 
 60                      class_threshold: str | int | None=None, 
 61                      train_ratio: float=0.8):
 62    """
 63    Function for calculating the training and testing cell positions 
 64    for multiclass data sets.
 65
 66    Parameters
 67    ----------
 68    y : array_like
 69        Should be an iterable object cooresponding to samples in 
 70        `ad.AnnData` object.
 71
 72    seed_obj : np.random._generator.Generator
 73        Seed used to randomly sample and split data.
 74
 75    train_ratio : float
 76        Ratio of number of training samples to entire data set. 
 77        Note: if a threshold is applied, the ratio training samples 
 78        may decrease depending on class balance and `class_threshold`
 79        parameter.
 80
 81    class_threshold : str | int
 82        If is type `int`, classes with more samples than 
 83        class_threshold will be sampled. If `'median'`, 
 84        samples will be sampled to the median number of samples per 
 85        class.
 86
 87    Returns
 88    -------
 89    train_indices : np.ndarray
 90        Indices for training samples.
 91
 92    test_indices : np.ndarray
 93        Indices for testing samples.
 94    """
 95    uniq_labels = np.unique(y)
 96
 97    # Finding indices for each cell class
 98    class_positions = {class_ : np.where(y == class_)[0] 
 99                       for class_ in uniq_labels}
100    
101    # Capturing training indices while maintaining original class proportions
102    train_samples = {class_ : seed_obj.choice(class_positions[class_], 
103                                              int(len(class_positions[class_])
104                                                  * train_ratio), 
105                                              replace = False)
106                        for class_ in class_positions.keys()}
107    
108    # Capturing testing indices while maintaining original class proportions
109    test_samples = {class_ : np.setdiff1d(class_positions[class_], 
110                                          train_samples[class_])
111                    for class_ in class_positions.keys()}
112    
113    # Applying threshold for samples per class
114    if class_threshold == 'median':
115        cells_per_class = [len(values) for values in train_samples.values()]
116        class_threshold = int(np.median(cells_per_class))
117
118    if isinstance(class_threshold, int):
119        # Down sample to class_threshold
120        for class_ in train_samples.keys():
121            if len(train_samples[class_]) > class_threshold:
122                train_samples[class_] = seed_obj.choice(train_samples[class_], 
123                                                        class_threshold)
124            
125    train_indices = np.array([idx for class_ in train_samples.keys()
126                                  for idx in train_samples[class_]])
127    
128    test_indices = np.array([idx for class_ in test_samples.keys()
129                                 for idx in test_samples[class_]])
130    
131    return train_indices, test_indices
132
133
134def binary_split(y: np.ndarray, train_indices: np.ndarray | None=None, 
135                  train_ratio: float=0.8,
136                  seed_obj: np.random._generator.Generator=np.random.default_rng(100)):
137    """
138    Function to calculate training and testing indices for given 
139    dataset. If train indices are given, it will calculate the test 
140    indices. If train_indices == None, then it calculates both indices, 
141    preserving the ratio of each label in y
142
143    Parameters
144    ----------
145    y : np.ndarray
146        Numpy array of cell labels. Can have any number of classes 
147        for this function.
148
149    train_indices : np.ndarray | None
150        Optional array of pre-determined training indices
151
152    train_ratio : float
153        Decimal value ratio of features in training/testing sets
154
155    seed_obj : np.random._generator.Generator
156        Numpy random state used for random processes. Can be 
157        specified for reproducubility or set by default.
158    
159    
160    Returns
161    -------
162    train_indices : np.ndarray
163        Array of indices of training cells.
164
165    test_indices : np.ndarray:
166        Array of indices of testing cells.
167    """
168    # If train indices aren't provided
169    if train_indices is None:
170
171        unique_labels = np.unique(y)
172        train_indices = []
173
174        for label in unique_labels:
175
176            # Find indices of each unique label
177            label_indices = np.where(y == label)[0]
178
179            # Sample these indices according to train ratio
180            n = int(len(label_indices) * train_ratio)
181            train_label_indices = seed_obj.choice(label_indices, n, 
182                                                  replace = False)
183            train_indices.extend(train_label_indices)
184    else:
185        assert len(train_indices) <= len(y), ("More train indices than there "
186                                              "are samples")
187
188    train_indices = np.array(train_indices)
189
190    # Test indices are the indices not in the train_indices
191    test_indices = np.setdiff1d(np.arange(len(y)), train_indices, 
192                                assume_unique = True)
193
194    return train_indices, test_indices
195
196
197def get_median_size(adata: ad.AnnData, other_factor: float=1.5):
198    """
199    Returns the median size of training plus testing samples per cell 
200    type. Used to calculate D for multiclass runs.
201
202    Parameters
203    ----------
204    adata : ad.AnnData
205        An ad.AnnData object with `test_indices` in `.uns` keys and 
206        `labels` in `.obs` keys.
207
208    Returns
209    -------
210    median_size : int
211        The median size of training plus testing samples across cell 
212        types. 
213    """
214    n_test = adata.uns['test_indices'].shape[0]
215
216    _, n_cts = np.unique(adata.obs['labels'][adata.uns['train_indices']], 
217                         return_counts=True)
218    sizes = [n_test + (other_factor*count) for count in n_cts]
219
220    return np.median(sizes)
221 
222
223def calculate_d(num_samples : int):
224    """
225    This function calculates the optimal number of dimensions for 
226    performance. See https://doi.org/10.48550/arXiv.1806.09178 for more
227    information.
228
229    Parameters
230    ----------
231    num_samples : int
232        The number of samples in the data set including both training
233        and testing sets.
234
235    Returns
236    -------
237    d : int
238        The optimal number of dimensions to run scMKL with the given 
239        data set.
240
241    Examples
242    --------
243    >>> raw_counts = scipy.sparse.load_npz('MCF7_counts.npz')
244    >>>
245    >>> num_cells = raw_counts.shape[0]
246    >>> d = scmkl.calculate_d(num_cells)
247    >>> d
248    161
249    """
250    d = int(np.sqrt(num_samples)*np.log(np.log(num_samples)))
251
252    return int(np.max([d, 100]))
253
254
255def get_optimal_d(adata: ad.AnnData, D: int | None, allow_multiclass: bool, 
256                  other_factor: float=1.5):
257    """
258    Takes the ad.AnnData object and input D. If D is type `int`, D will 
259    be return. If D is `None` and `allow_multiclass is False`, 
260    `scmkl.calculate_d(adata.shape[0])` will be returned. Else, median 
261    size of training and testing will be calculated and 
262    `scmkl.calculate_d(median_size)` will be returned.
263
264    Parameters
265    ----------
266    adata : ad.AnnData
267        An ad.AnnData object with `test_indices` in `.uns` keys and 
268        `labels` in `.obs` keys.
269    
270    D : int | None
271        The D provided as an `int` or `None` if optimal D should be 
272        calculated.
273
274    allow_multiclass : bool
275        Should be `False` if labels are binary. Else, should be `True` 
276        indicating there are more than two classes.
277
278    Returns
279    -------
280    d : int
281        Either the input or calculated optimal d for the experiment. 
282    """
283    if D is not None:    
284        assert isinstance(D, int) and D > 0, 'D must be a positive integer.'
285        return D
286    
287    if allow_multiclass:
288        size = get_median_size(adata, other_factor)
289    else:
290        size = adata.shape[0]
291
292    return calculate_d(size)
293        
294
295def sort_samples(train_indices, test_indices):
296    """
297    Ensures that samples in adata obj are all training, then all 
298    testing.
299
300    Parameters
301    ----------
302    train_indices : np.ndarray
303        Indices in ad.AnnData object for training.
304    
305    test_indices : np.ndarray
306        Indices in ad.AnnData object for testing.
307
308    Returns
309    -------
310    sort_idc : np.ndarray
311        Ordered indices that will sort ad.AnnData object as all 
312        training samples, then all testing.
313
314    train_indices : np.ndarray
315        The new training indices given the new index order, `sort_idc`.
316
317    test_indices : np.ndarray
318        The new testing indices given the new index order, `sort_idc`.
319    """
320    sort_idc = np.concatenate([train_indices, test_indices])
321
322    train_indices = np.arange(0, train_indices.shape[0])
323    test_indices = np.arange(train_indices.shape[0], 
324                             train_indices.shape[0] + test_indices.shape[0])
325    
326    return sort_idc, train_indices, test_indices
327
328
329def create_adata(X: scipy.sparse._csc.csc_matrix | np.ndarray | pd.DataFrame, 
330                 feature_names: np.ndarray, cell_labels: np.ndarray, 
331                 group_dict: dict, obs_names: None | np.ndarray=None, 
332                 scale_data: bool=True, transform_data: bool=False, 
333                 split_data: np.ndarray | None=None, D: int | None=None, 
334                 remove_features: bool=True, train_ratio: float=0.8, 
335                 distance_metric: str='euclidean', kernel_type: str='Gaussian', 
336                 random_state: int=1, allow_multiclass: bool = False, 
337                 class_threshold: str | int | None = None,
338                 reduction: str | None = None, tfidf: bool = False, 
339                 other_factor: float=1.5, add_ones: bool=False):
340    """
341    Function to create an AnnData object to carry all relevant 
342    information going forward.
343
344    Parameters
345    ----------
346    X : scipy.sparse.csc_matrix | np.ndarray | pd.DataFrame
347        A data matrix of cells by features (sparse array 
348        recommended for large datasets).
349
350    feature_names : np.ndarray
351        Array of feature names corresponding with the features 
352        in `X`.
353
354    cell_labels : np.ndarray
355        A numpy array of cell phenotypes corresponding with 
356        the cells in `X`.
357
358    group_dict : dict 
359        Dictionary containing feature grouping information (i.e. 
360        `{geneset1: np.array([gene_1, gene_2, ..., gene_n]), geneset2: 
361        np.array([...]), ...}`.
362
363    obs_names : None | np.ndarray
364        The cell names corresponding to `X` to be assigned to output 
365        object `.obs_names` attribute.
366
367    scale_data : bool  
368        If `True`, data matrix is log transformed and standard 
369        scaled. Default is `True`.
370
371    transform_data : bool
372        If `True`, data will be log1p transformed (recommended for 
373        counts data). Default is `False`.   
374    
375    split_data : None | np.ndarray
376        If `None`, data will be split stratified by cell labels. 
377        Else, is an array of precalculated train/test split 
378        corresponding to samples. Can include labels for entire
379        dataset to benchmark performance or for only training
380        data to classify unknown cell types (i.e. `np.array(['train', 
381        'test', ..., 'train'])`.
382
383    D : int 
384        Number of Random Fourier Features used to calculate Z. 
385        Should be a positive integer. Higher values of D will 
386        increase classification accuracy at the cost of computation 
387        time. If set to `None`, will be calculated given number of 
388        samples. 
389    
390    remove_features : bool
391        If `True`, will remove features from `X` and `feature_names` 
392        not in `group_dict` and remove features from groupings not in 
393        `feature_names`.
394
395    train_ratio : float
396        Ratio of number of training samples to entire data set. Note:
397        if a threshold is applied, the ratio training samples may 
398        decrease depending on class balance and `class_threshold`
399        parameter if `allow_multiclass = True`.
400
401    distance_metric : str
402        The pairwise distance metric used to estimate sigma. Must
403        be one of the options used in `scipy.spatial.distance.cdist`.
404
405    kernel_type : str
406        The approximated kernel function used to calculate Zs.
407        Must be one of `'Gaussian'`, `'Laplacian'`, or `'Cauchy'`.
408
409    random_state : int
410        Integer random_state used to set the seed for 
411        reproducibilty.
412
413    allow_multiclass : bool
414        If `False`, will ensure that cell labels are binary.
415
416    class_threshold : str | int
417        Number of samples allowed in the training data for each cell
418        class in the training data. If `'median'`, the median number 
419        of cells per cell class will be the threshold for number of 
420        samples per class.
421
422    reduction: str | None
423        Choose which dimension reduction technique to perform on 
424        features within a group. 'svd' will run 
425        `sklearn.decomposition.TruncatedSVD`, 'linear' will multiply 
426        by an array of 1s down to 50 dimensions. 'pca' will replace 
427        each group values with 50 PCs from principal component 
428        analysis.
429        
430    tfidf: bool
431        Whether to calculate TFIDF transformation on peaks within 
432        groupings.
433
434    add_ones: bool
435        Allows the addition of ones for downstream functions to run 
436        single features.
437        
438    Returns
439    -------
440    adata : ad.AnnData
441        AnnData with the following attributes and keys:
442
443        `adata.X` (array_like):
444            Data matrix.
445    
446        `adata.var_names` (array_like): 
447            Feature names corresponding to `adata.X`.
448
449        `adata.obs['labels']` (array_like):
450            cell classes/phenotypes from `cell_labels`.
451
452        `adata.uns['train_indices']` (array_like):
453            Indices for training data. 
454
455        `adata.uns['test_indices']` (array_like)
456            Indices for testing data.
457
458        `adata.uns['group_dict']` (dict):
459            Grouping information.
460
461        `adata.uns['seed_obj']` (np.random._generator.Generator): 
462            Seed object with seed equal to 100 * `random_state`.
463
464        `adata.uns['D']` (int):
465            Number of dimensions to scMKL with.
466
467        `adata.uns['scale_data']` (bool):
468            Whether or not data is scaled.
469
470        `adata.uns['transform_data']` (bool):
471            Whether or not data is log1p transformed.
472
473        `adata.uns['distance_metric']` (str): 
474            Distance metric as given.
475    
476        `adata.uns['kernel_type']` (str): 
477            Kernel function as given.
478
479        `adata.uns['svd']` (bool): 
480            Whether to calculate SVD reduction.
481
482        `adata.uns['tfidf']` (bool): 
483            Whether to calculate TF-IDF per grouping.
484
485    Examples
486    --------
487    >>> data_mat = scipy.sparse.load_npz('MCF7_RNA_matrix.npz')
488    >>> gene_names = np.load('MCF7_gene_names.pkl', allow_pickle = True)
489    >>> group_dict = np.load('hallmark_genesets.pkl', 
490    >>>                      allow_pickle = True)
491    >>> 
492    >>> adata = scmkl.create_adata(X = data_mat, 
493    ...                            feature_names = gene_names, 
494    ...                            group_dict = group_dict)
495    >>> adata
496    AnnData object with n_obs × n_vars = 1000 × 4341
497    obs: 'labels'
498    uns: 'group_dict', 'seed_obj', 'scale_data', 'D', 'kernel_type', 
499    'distance_metric', 'train_indices', 'test_indices'
500    """
501    assert X.shape[1] == len(feature_names), ("Different number of features "
502                                              "in X than feature names.")
503    
504    if not allow_multiclass:
505        assert len(np.unique(cell_labels)) == 2, ("cell_labels must contain "
506                                                  "2 classes.")
507
508    kernel_options = ['gaussian', 'laplacian', 'cauchy']
509    assert kernel_type.lower() in kernel_options, ("Given kernel type not "
510                                                   "implemented. Gaussian, "
511                                                   "Laplacian, and Cauchy "
512                                                   "are the acceptable "
513                                                   "types.")
514
515    # Create adata object and add column names
516    adata = ad.AnnData(X)
517    adata.var_names = feature_names
518
519    if isinstance(obs_names, (np.ndarray)):
520        adata.obs_names = obs_names
521
522    filtered_feature_names, group_dict = filter_features(feature_names, 
523                                                          group_dict,
524                                                          add_ones)
525    
526    # Ensuring that there are common features between feature_names and groups
527    overlap_err = ("No common features between feature names and grouping "
528                   "dict. Check grouping.")
529    assert len(filtered_feature_names) > 0, overlap_err
530
531    if remove_features:
532        warnings.filterwarnings('ignore', category = ad.ImplicitModificationWarning)
533        adata = adata[:, filtered_feature_names]
534    
535    gc.collect()
536
537    # Add metadata to adata object
538    adata.uns['group_dict'] = group_dict
539    adata.uns['seed_obj'] = np.random.default_rng(100*random_state)
540    adata.uns['scale_data'] = scale_data
541    adata.uns['transform_data'] = transform_data
542    adata.uns['kernel_type'] = kernel_type
543    adata.uns['distance_metric'] = distance_metric
544    adata.uns['reduction'] = reduction if isinstance(reduction, str) else 'None'
545    adata.uns['tfidf'] = tfidf
546    adata.uns['add_ones'] = add_ones
547
548    if (split_data is None):
549        assert X.shape[0] == len(cell_labels), ("Different number of cells "
550                                                "than labels")
551        adata.obs['labels'] = cell_labels
552
553        if (allow_multiclass == False):
554            split = binary_split(cell_labels, 
555                                  seed_obj = adata.uns['seed_obj'],
556                                  train_ratio = train_ratio)
557            train_indices, test_indices = split
558
559        elif (allow_multiclass == True):
560            split = multi_class_split(cell_labels, 
561                                       seed_obj = adata.uns['seed_obj'], 
562                                       class_threshold = class_threshold,
563                                       train_ratio = train_ratio)
564            train_indices, test_indices = split
565
566        adata.uns['labeled_test'] = True
567
568    else:
569        sd_err_message = "`split_data` argument must be of type np.ndarray"
570        assert isinstance(split_data, np.ndarray), sd_err_message
571        x_eq_labs = X.shape[0] == len(cell_labels)
572        train_eq_labs = X.shape[0] == len(cell_labels)
573        assert x_eq_labs or train_eq_labs, ("Must give labels for all cells "
574                                            "or only for training cells")
575        
576        train_indices = np.where(split_data == 'train')[0]
577        test_indices = np.where(split_data == 'test')[0]
578
579        if len(cell_labels) == len(train_indices):
580
581            padded_cell_labels = np.zeros((X.shape[0])).astype('object')
582            padded_cell_labels[train_indices] = cell_labels
583            padded_cell_labels[test_indices] = 'padded_test_label'
584
585            adata.obs['labels'] = padded_cell_labels
586            adata.uns['labeled_test'] = False
587
588        elif len(cell_labels) == len(split_data):
589            adata.obs['labels'] = cell_labels
590            adata.uns['labeled_test'] = True
591
592    # Ensuring all train samples are first in adata object followed by test
593    sort_idx, train_indices, test_indices = sort_samples(train_indices, 
594                                                         test_indices)
595
596    adata = adata[sort_idx]
597
598    if not isinstance(obs_names, (np.ndarray)):
599        adata.obs = adata.obs.reset_index(drop=True)
600        adata.obs.index = adata.obs.index.astype('O')
601
602    adata.uns['train_indices'] = train_indices
603    adata.uns['test_indices'] = test_indices
604
605    adata.uns['D'] = get_optimal_d(adata, D, allow_multiclass, other_factor)
606
607    cts, cts_counts = np.unique(adata.obs['labels'], return_counts=True)
608    small_cts = cts[10 > cts_counts]
609    if small_cts.size:
610        print("WARNING: There are less than 10 cells for the "
611              "following label(s):", small_cts, 
612              "Can lead to issues running `scmkl.optimize_alpha()`.", 
613              sep='\n')
614    if not scale_data:
615        print("WARNING: Data will not be scaled. "
616              "To change this behavior, set scale_data to True")
617    if not transform_data:
618        print("WARNING: Data will not be transformed."
619              "To change this behavior, set transform_data to True")
620        
621    # Need to avoid including samples with no obervations 
622    # (must be done after feature filtering for grouping)
623    zero_var = np.array(sparse_var(adata.X, axis=1) == 0).ravel()
624    num_zero_var = np.sum(zero_var)
625    if 0 < num_zero_var:
626        print("WARNING: Some samples contain no variance across features "
627              "(likely all zeros), consider removing them and recreating "
628              "AnnData.", flush=True)
629
630    return adata
631
632
633def format_adata(adata: ad.AnnData | str, cell_labels: np.ndarray | str, 
634                 group_dict: dict | str, use_raw: bool=False, 
635                 scale_data: bool=True, transform_data: bool=False, 
636                 split_data: np.ndarray | None=None, D: int | None=None, 
637                 remove_features: bool=True, train_ratio: float=0.8, 
638                 distance_metric: str='euclidean', kernel_type: str='Gaussian', 
639                 random_state: int=1, allow_multiclass: bool = False, 
640                 class_threshold: str | int | None=None, 
641                 reduction: str | None=None, tfidf: bool=False,
642                 other_factor: float=1.5, add_ones: bool=False):
643    """
644    Function to format an `ad.AnnData` object to carry all relevant 
645    information going forward. `adata.obs_names` will be retained.
646
647    **NOTE: Information not needed for running `scmkl` will be 
648    removed.**
649
650    Parameters
651    ----------
652    adata : ad.AnnData
653        Object with data for `scmkl` to be applied to. Only requirment 
654        is that `.var_names` is correct and data matrix is in `adata.X` 
655        or `adata.raw.X`. A h5ad file can be provided as a `str` and it 
656        will be read in.
657
658    cell_labels : np.ndarray | str
659        If type `str`, the labels for `scmkl` to learn are captured 
660        from `adata.obs['cell_labels']`. Else, a `np.ndarray` of cell 
661        phenotypes corresponding with the cells in `adata.X`.
662
663    group_dict : dict | str
664        Dictionary containing feature grouping information (i.e. 
665        `{geneset1: np.array([gene_1, gene_2, ..., gene_n]), geneset2: 
666        np.array([...]), ...}`. A pickle file can be provided as a `str` 
667        and it will be read in.
668
669    obs_names : None | np.ndarray
670        The cell names corresponding to `X` to be assigned to output 
671        object `.obs_names` attribute.
672
673    use_raw : bool
674        If `False`, will use `adata.X` to create new `adata`. Else, 
675        will use `adata.raw.X`.
676
677    scale_data : bool  
678        If `True`, data matrix is log transformed and standard 
679        scaled. 
680
681    transform_data : bool
682        If `True`, data will be log1p transformed (recommended for 
683        counts data). Default is `False`. 
684        
685    split_data : None | np.ndarray
686        If `None`, data will be split stratified by cell labels. 
687        Else, is an array of precalculated train/test split 
688        corresponding to samples. Can include labels for entire
689        dataset to benchmark performance or for only training
690        data to classify unknown cell types (i.e. `np.array(['train', 
691        'test', ..., 'train'])`.
692
693    D : int 
694        Number of Random Fourier Features used to calculate Z. 
695        Should be a positive integer. Higher values of D will 
696        increase classification accuracy at the cost of computation 
697        time. If set to `None`, will be calculated given number of 
698        samples. 
699    
700    remove_features : bool
701        If `True`, will remove features from `X` and `feature_names` 
702        not in `group_dict` and remove features from groupings not in 
703        `feature_names`.
704
705    train_ratio : float
706        Ratio of number of training samples to entire data set. Note:
707        if a threshold is applied, the ratio training samples may 
708        decrease depending on class balance and `class_threshold`
709        parameter if `allow_multiclass = True`.
710
711    distance_metric : str
712        The pairwise distance metric used to estimate sigma. Must
713        be one of the options used in `scipy.spatial.distance.cdist`.
714
715    kernel_type : str
716        The approximated kernel function used to calculate Zs.
717        Must be one of `'Gaussian'`, `'Laplacian'`, or `'Cauchy'`.
718
719    random_state : int
720        Integer random_state used to set the seed for 
721        reproducibilty.
722
723    allow_multiclass : bool
724        If `False`, will ensure that cell labels are binary.
725
726    class_threshold : str | int
727        Number of samples allowed in the training data for each cell
728        class in the training data. If `'median'`, the median number 
729        of cells per cell class will be the threshold for number of 
730        samples per class.
731
732    reduction: str | None
733        Choose which dimension reduction technique to perform on 
734        features within a group. 'svd' will run 
735        `sklearn.decomposition.TruncatedSVD`, 'linear' will multiply 
736        by an array of 1s down to 50 dimensions.
737        
738    tfidf: bool
739        Whether to calculate TFIDF transformation on peaks within 
740        groupings.
741        
742    Returns
743    -------
744    adata : ad.AnnData
745        AnnData with the following attributes and keys:
746
747        `adata.X` (array_like):
748            Data matrix.
749    
750        `adata.var_names` (array_like): 
751            Feature names corresponding to `adata.X`.
752
753        `adata.obs['labels']` (array_like):
754            cell classes/phenotypes from `cell_labels`.
755
756        `adata.uns['train_indices']` (array_like):
757            Indices for training data. 
758
759        `adata.uns['test_indices']` (array_like)
760            Indices for testing data.
761
762        `adata.uns['group_dict']` (dict):
763            Grouping information.
764
765        `adata.uns['seed_obj']` (np.random._generator.Generator): 
766            Seed object with seed equal to 100 * `random_state`.
767
768        `adata.uns['D']` (int):
769            Number of dimensions to scMKL with.
770
771        `adata.uns['scale_data']` (bool):
772            Whether or not data is scaled.
773
774        `adata.uns['transform_data']` (bool):
775            Whether or not data is log1p transformed.
776
777        `adata.uns['distance_metric']` (str): 
778            Distance metric as given.
779    
780        `adata.uns['kernel_type']` (str): 
781            Kernel function as given.
782
783        `adata.uns['svd']` (bool): 
784            Whether to calculate SVD reduction.
785
786        `adata.uns['tfidf']` (bool): 
787            Whether to calculate TF-IDF per grouping.
788
789    Examples
790    --------
791    >>> adata = ad.read_h5ad('MCF7_rna.h5ad')
792    >>> group_dict = np.load('hallmark_genesets.pkl', 
793    >>>                      allow_pickle = True)
794    >>> 
795    >>> 
796    >>> # The labels in adata.obs we want to learn are 'celltypes'
797    >>> adata = scmkl.format_adata(adata, 'celltypes', 
798    ...                            group_dict)
799    >>> adata
800    AnnData object with n_obs × n_vars = 1000 × 4341
801    obs: 'labels'
802    uns: 'group_dict', 'seed_obj', 'scale_data', 'D', 'kernel_type', 
803    'distance_metric', 'train_indices', 'test_indices'
804    """
805    if str == type(adata):
806        adata = ad.read_h5ad(adata)
807
808    if str == type(group_dict):
809        group_dict = np.load(group_dict, allow_pickle=True)
810        
811    if str == type(cell_labels):
812        err_msg = f"{cell_labels} is not in `adata.obs`"
813        assert cell_labels in adata.obs.keys(), err_msg
814        cell_labels = adata.obs[cell_labels].to_numpy()
815    
816    if use_raw:
817        assert adata.raw, "`adata.raw` is empty, set `use_raw` to `False`"
818        X = adata.raw.X
819    else:
820        X = adata.X
821
822    adata = create_adata(X, adata.var_names.to_numpy().copy(), cell_labels, 
823                         group_dict, adata.obs_names.to_numpy().copy(), 
824                         scale_data, transform_data, split_data, D, remove_features, 
825                         train_ratio, distance_metric, kernel_type, 
826                         random_state, allow_multiclass, class_threshold, 
827                         reduction, tfidf, other_factor, add_ones)
828
829    return adata
def filter_features(feature_names: numpy.ndarray, group_dict: dict, add_ones: bool):
12def filter_features(feature_names: np.ndarray, group_dict: dict,
13                    add_ones: bool):
14    """
15    Function to remove features only in feature names or group_dict.
16    Any features not included in group_dict will be removed from the
17    matrix. Also puts the features in the same relative order (of 
18    included features)
19    
20    Parameters
21    ----------
22    feature_names : np.ndarray
23        Numpy array of corresponding feature names.
24
25    group_dict : dict
26        Dictionary containing feature grouping information.
27                 Example: {geneset: np.array(gene_1, gene_2, ..., 
28                 gene_n)}
29    Returns
30    -------
31    feature_names : np.ndarray
32        Numpy array of corresponding feature names from group_dict.
33
34    group_dict : dict
35        Dictionary containing features overlapping input grouping 
36        information and full feature names.
37    """
38    group_features = set()
39    feature_set = set(feature_names)
40
41    # Store all objects in dictionary in set
42    for group in group_dict.keys():
43        group_features.update(set(group_dict[group]))
44
45        # Finds intersection between group features and features in data
46        # Converts to nd.array and sorts to preserve order of feature names
47        group_feats = list(feature_set.intersection(set(group_dict[group])))
48        group_dict[group] = np.sort(np.array(group_feats))
49
50    # Only keeping groupings that have at least two features
51    min_features = 1 if add_ones else 2
52    group_dict = {group : group_dict[group] for group in group_dict.keys()
53                  if len(group_dict[group]) >= min_features}
54
55    group_features = np.array(list(group_features.intersection(feature_set)))
56
57    return group_features, group_dict

Function to remove features only in feature names or group_dict. Any features not included in group_dict will be removed from the matrix. Also puts the features in the same relative order (of included features)

Parameters
  • feature_names (np.ndarray): Numpy array of corresponding feature names.
  • group_dict (dict): Dictionary containing feature grouping information. Example: {geneset: np.array(gene_1, gene_2, ..., gene_n)}
Returns
  • feature_names (np.ndarray): Numpy array of corresponding feature names from group_dict.
  • group_dict (dict): Dictionary containing features overlapping input grouping information and full feature names.
def multi_class_split( y: numpy.ndarray, seed_obj: numpy.random._generator.Generator, class_threshold: str | int | None = None, train_ratio: float = 0.8):
 60def multi_class_split(y: np.ndarray, seed_obj: np.random._generator.Generator, 
 61                      class_threshold: str | int | None=None, 
 62                      train_ratio: float=0.8):
 63    """
 64    Function for calculating the training and testing cell positions 
 65    for multiclass data sets.
 66
 67    Parameters
 68    ----------
 69    y : array_like
 70        Should be an iterable object cooresponding to samples in 
 71        `ad.AnnData` object.
 72
 73    seed_obj : np.random._generator.Generator
 74        Seed used to randomly sample and split data.
 75
 76    train_ratio : float
 77        Ratio of number of training samples to entire data set. 
 78        Note: if a threshold is applied, the ratio training samples 
 79        may decrease depending on class balance and `class_threshold`
 80        parameter.
 81
 82    class_threshold : str | int
 83        If is type `int`, classes with more samples than 
 84        class_threshold will be sampled. If `'median'`, 
 85        samples will be sampled to the median number of samples per 
 86        class.
 87
 88    Returns
 89    -------
 90    train_indices : np.ndarray
 91        Indices for training samples.
 92
 93    test_indices : np.ndarray
 94        Indices for testing samples.
 95    """
 96    uniq_labels = np.unique(y)
 97
 98    # Finding indices for each cell class
 99    class_positions = {class_ : np.where(y == class_)[0] 
100                       for class_ in uniq_labels}
101    
102    # Capturing training indices while maintaining original class proportions
103    train_samples = {class_ : seed_obj.choice(class_positions[class_], 
104                                              int(len(class_positions[class_])
105                                                  * train_ratio), 
106                                              replace = False)
107                        for class_ in class_positions.keys()}
108    
109    # Capturing testing indices while maintaining original class proportions
110    test_samples = {class_ : np.setdiff1d(class_positions[class_], 
111                                          train_samples[class_])
112                    for class_ in class_positions.keys()}
113    
114    # Applying threshold for samples per class
115    if class_threshold == 'median':
116        cells_per_class = [len(values) for values in train_samples.values()]
117        class_threshold = int(np.median(cells_per_class))
118
119    if isinstance(class_threshold, int):
120        # Down sample to class_threshold
121        for class_ in train_samples.keys():
122            if len(train_samples[class_]) > class_threshold:
123                train_samples[class_] = seed_obj.choice(train_samples[class_], 
124                                                        class_threshold)
125            
126    train_indices = np.array([idx for class_ in train_samples.keys()
127                                  for idx in train_samples[class_]])
128    
129    test_indices = np.array([idx for class_ in test_samples.keys()
130                                 for idx in test_samples[class_]])
131    
132    return train_indices, test_indices

Function for calculating the training and testing cell positions for multiclass data sets.

Parameters
  • y (array_like): Should be an iterable object cooresponding to samples in ad.AnnData object.
  • seed_obj (np.random._generator.Generator): Seed used to randomly sample and split data.
  • train_ratio (float): Ratio of number of training samples to entire data set. Note: if a threshold is applied, the ratio training samples may decrease depending on class balance and class_threshold parameter.
  • class_threshold (str | int): If is type int, classes with more samples than class_threshold will be sampled. If 'median', samples will be sampled to the median number of samples per class.
Returns
  • train_indices (np.ndarray): Indices for training samples.
  • test_indices (np.ndarray): Indices for testing samples.
def binary_split( y: numpy.ndarray, train_indices: numpy.ndarray | None = None, train_ratio: float = 0.8, seed_obj: numpy.random._generator.Generator = Generator(PCG64) at 0x757C5DFB49E0):
135def binary_split(y: np.ndarray, train_indices: np.ndarray | None=None, 
136                  train_ratio: float=0.8,
137                  seed_obj: np.random._generator.Generator=np.random.default_rng(100)):
138    """
139    Function to calculate training and testing indices for given 
140    dataset. If train indices are given, it will calculate the test 
141    indices. If train_indices == None, then it calculates both indices, 
142    preserving the ratio of each label in y
143
144    Parameters
145    ----------
146    y : np.ndarray
147        Numpy array of cell labels. Can have any number of classes 
148        for this function.
149
150    train_indices : np.ndarray | None
151        Optional array of pre-determined training indices
152
153    train_ratio : float
154        Decimal value ratio of features in training/testing sets
155
156    seed_obj : np.random._generator.Generator
157        Numpy random state used for random processes. Can be 
158        specified for reproducubility or set by default.
159    
160    
161    Returns
162    -------
163    train_indices : np.ndarray
164        Array of indices of training cells.
165
166    test_indices : np.ndarray:
167        Array of indices of testing cells.
168    """
169    # If train indices aren't provided
170    if train_indices is None:
171
172        unique_labels = np.unique(y)
173        train_indices = []
174
175        for label in unique_labels:
176
177            # Find indices of each unique label
178            label_indices = np.where(y == label)[0]
179
180            # Sample these indices according to train ratio
181            n = int(len(label_indices) * train_ratio)
182            train_label_indices = seed_obj.choice(label_indices, n, 
183                                                  replace = False)
184            train_indices.extend(train_label_indices)
185    else:
186        assert len(train_indices) <= len(y), ("More train indices than there "
187                                              "are samples")
188
189    train_indices = np.array(train_indices)
190
191    # Test indices are the indices not in the train_indices
192    test_indices = np.setdiff1d(np.arange(len(y)), train_indices, 
193                                assume_unique = True)
194
195    return train_indices, test_indices

Function to calculate training and testing indices for given dataset. If train indices are given, it will calculate the test indices. If train_indices == None, then it calculates both indices, preserving the ratio of each label in y

Parameters
  • y (np.ndarray): Numpy array of cell labels. Can have any number of classes for this function.
  • train_indices (np.ndarray | None): Optional array of pre-determined training indices
  • train_ratio (float): Decimal value ratio of features in training/testing sets
  • seed_obj (np.random._generator.Generator): Numpy random state used for random processes. Can be specified for reproducubility or set by default.
Returns
  • train_indices (np.ndarray): Array of indices of training cells.
  • test_indices (np.ndarray:): Array of indices of testing cells.
def get_median_size(adata: anndata._core.anndata.AnnData, other_factor: float = 1.5):
198def get_median_size(adata: ad.AnnData, other_factor: float=1.5):
199    """
200    Returns the median size of training plus testing samples per cell 
201    type. Used to calculate D for multiclass runs.
202
203    Parameters
204    ----------
205    adata : ad.AnnData
206        An ad.AnnData object with `test_indices` in `.uns` keys and 
207        `labels` in `.obs` keys.
208
209    Returns
210    -------
211    median_size : int
212        The median size of training plus testing samples across cell 
213        types. 
214    """
215    n_test = adata.uns['test_indices'].shape[0]
216
217    _, n_cts = np.unique(adata.obs['labels'][adata.uns['train_indices']], 
218                         return_counts=True)
219    sizes = [n_test + (other_factor*count) for count in n_cts]
220
221    return np.median(sizes)

Returns the median size of training plus testing samples per cell type. Used to calculate D for multiclass runs.

Parameters
  • adata (ad.AnnData): An ad.AnnData object with test_indices in .uns keys and labels in .obs keys.
Returns
  • median_size (int): The median size of training plus testing samples across cell types.
def calculate_d(num_samples: int):
224def calculate_d(num_samples : int):
225    """
226    This function calculates the optimal number of dimensions for 
227    performance. See https://doi.org/10.48550/arXiv.1806.09178 for more
228    information.
229
230    Parameters
231    ----------
232    num_samples : int
233        The number of samples in the data set including both training
234        and testing sets.
235
236    Returns
237    -------
238    d : int
239        The optimal number of dimensions to run scMKL with the given 
240        data set.
241
242    Examples
243    --------
244    >>> raw_counts = scipy.sparse.load_npz('MCF7_counts.npz')
245    >>>
246    >>> num_cells = raw_counts.shape[0]
247    >>> d = scmkl.calculate_d(num_cells)
248    >>> d
249    161
250    """
251    d = int(np.sqrt(num_samples)*np.log(np.log(num_samples)))
252
253    return int(np.max([d, 100]))

This function calculates the optimal number of dimensions for performance. See https://doi.org/10.48550/arXiv.1806.09178 for more information.

Parameters
  • num_samples (int): The number of samples in the data set including both training and testing sets.
Returns
  • d (int): The optimal number of dimensions to run scMKL with the given data set.
Examples
>>> raw_counts = scipy.sparse.load_npz('MCF7_counts.npz')
>>>
>>> num_cells = raw_counts.shape[0]
>>> d = scmkl.calculate_d(num_cells)
>>> d
161
def get_optimal_d( adata: anndata._core.anndata.AnnData, D: int | None, allow_multiclass: bool, other_factor: float = 1.5):
256def get_optimal_d(adata: ad.AnnData, D: int | None, allow_multiclass: bool, 
257                  other_factor: float=1.5):
258    """
259    Takes the ad.AnnData object and input D. If D is type `int`, D will 
260    be return. If D is `None` and `allow_multiclass is False`, 
261    `scmkl.calculate_d(adata.shape[0])` will be returned. Else, median 
262    size of training and testing will be calculated and 
263    `scmkl.calculate_d(median_size)` will be returned.
264
265    Parameters
266    ----------
267    adata : ad.AnnData
268        An ad.AnnData object with `test_indices` in `.uns` keys and 
269        `labels` in `.obs` keys.
270    
271    D : int | None
272        The D provided as an `int` or `None` if optimal D should be 
273        calculated.
274
275    allow_multiclass : bool
276        Should be `False` if labels are binary. Else, should be `True` 
277        indicating there are more than two classes.
278
279    Returns
280    -------
281    d : int
282        Either the input or calculated optimal d for the experiment. 
283    """
284    if D is not None:    
285        assert isinstance(D, int) and D > 0, 'D must be a positive integer.'
286        return D
287    
288    if allow_multiclass:
289        size = get_median_size(adata, other_factor)
290    else:
291        size = adata.shape[0]
292
293    return calculate_d(size)

Takes the ad.AnnData object and input D. If D is type int, D will be return. If D is None and allow_multiclass is False, scmkl.calculate_d(adata.shape[0]) will be returned. Else, median size of training and testing will be calculated and scmkl.calculate_d(median_size) will be returned.

Parameters
  • adata (ad.AnnData): An ad.AnnData object with test_indices in .uns keys and labels in .obs keys.
  • D (int | None): The D provided as an int or None if optimal D should be calculated.
  • allow_multiclass (bool): Should be False if labels are binary. Else, should be True indicating there are more than two classes.
Returns
  • d (int): Either the input or calculated optimal d for the experiment.
def sort_samples(train_indices, test_indices):
296def sort_samples(train_indices, test_indices):
297    """
298    Ensures that samples in adata obj are all training, then all 
299    testing.
300
301    Parameters
302    ----------
303    train_indices : np.ndarray
304        Indices in ad.AnnData object for training.
305    
306    test_indices : np.ndarray
307        Indices in ad.AnnData object for testing.
308
309    Returns
310    -------
311    sort_idc : np.ndarray
312        Ordered indices that will sort ad.AnnData object as all 
313        training samples, then all testing.
314
315    train_indices : np.ndarray
316        The new training indices given the new index order, `sort_idc`.
317
318    test_indices : np.ndarray
319        The new testing indices given the new index order, `sort_idc`.
320    """
321    sort_idc = np.concatenate([train_indices, test_indices])
322
323    train_indices = np.arange(0, train_indices.shape[0])
324    test_indices = np.arange(train_indices.shape[0], 
325                             train_indices.shape[0] + test_indices.shape[0])
326    
327    return sort_idc, train_indices, test_indices

Ensures that samples in adata obj are all training, then all testing.

Parameters
  • train_indices (np.ndarray): Indices in ad.AnnData object for training.
  • test_indices (np.ndarray): Indices in ad.AnnData object for testing.
Returns
  • sort_idc (np.ndarray): Ordered indices that will sort ad.AnnData object as all training samples, then all testing.
  • train_indices (np.ndarray): The new training indices given the new index order, sort_idc.
  • test_indices (np.ndarray): The new testing indices given the new index order, sort_idc.
def create_adata( X: scipy.sparse._csc.csc_matrix | numpy.ndarray | pandas.core.frame.DataFrame, feature_names: numpy.ndarray, cell_labels: numpy.ndarray, group_dict: dict, obs_names: None | numpy.ndarray = None, scale_data: bool = True, transform_data: bool = False, split_data: numpy.ndarray | None = None, D: int | None = None, remove_features: bool = True, train_ratio: float = 0.8, distance_metric: str = 'euclidean', kernel_type: str = 'Gaussian', random_state: int = 1, allow_multiclass: bool = False, class_threshold: str | int | None = None, reduction: str | None = None, tfidf: bool = False, other_factor: float = 1.5, add_ones: bool = False):
330def create_adata(X: scipy.sparse._csc.csc_matrix | np.ndarray | pd.DataFrame, 
331                 feature_names: np.ndarray, cell_labels: np.ndarray, 
332                 group_dict: dict, obs_names: None | np.ndarray=None, 
333                 scale_data: bool=True, transform_data: bool=False, 
334                 split_data: np.ndarray | None=None, D: int | None=None, 
335                 remove_features: bool=True, train_ratio: float=0.8, 
336                 distance_metric: str='euclidean', kernel_type: str='Gaussian', 
337                 random_state: int=1, allow_multiclass: bool = False, 
338                 class_threshold: str | int | None = None,
339                 reduction: str | None = None, tfidf: bool = False, 
340                 other_factor: float=1.5, add_ones: bool=False):
341    """
342    Function to create an AnnData object to carry all relevant 
343    information going forward.
344
345    Parameters
346    ----------
347    X : scipy.sparse.csc_matrix | np.ndarray | pd.DataFrame
348        A data matrix of cells by features (sparse array 
349        recommended for large datasets).
350
351    feature_names : np.ndarray
352        Array of feature names corresponding with the features 
353        in `X`.
354
355    cell_labels : np.ndarray
356        A numpy array of cell phenotypes corresponding with 
357        the cells in `X`.
358
359    group_dict : dict 
360        Dictionary containing feature grouping information (i.e. 
361        `{geneset1: np.array([gene_1, gene_2, ..., gene_n]), geneset2: 
362        np.array([...]), ...}`.
363
364    obs_names : None | np.ndarray
365        The cell names corresponding to `X` to be assigned to output 
366        object `.obs_names` attribute.
367
368    scale_data : bool  
369        If `True`, data matrix is log transformed and standard 
370        scaled. Default is `True`.
371
372    transform_data : bool
373        If `True`, data will be log1p transformed (recommended for 
374        counts data). Default is `False`.   
375    
376    split_data : None | np.ndarray
377        If `None`, data will be split stratified by cell labels. 
378        Else, is an array of precalculated train/test split 
379        corresponding to samples. Can include labels for entire
380        dataset to benchmark performance or for only training
381        data to classify unknown cell types (i.e. `np.array(['train', 
382        'test', ..., 'train'])`.
383
384    D : int 
385        Number of Random Fourier Features used to calculate Z. 
386        Should be a positive integer. Higher values of D will 
387        increase classification accuracy at the cost of computation 
388        time. If set to `None`, will be calculated given number of 
389        samples. 
390    
391    remove_features : bool
392        If `True`, will remove features from `X` and `feature_names` 
393        not in `group_dict` and remove features from groupings not in 
394        `feature_names`.
395
396    train_ratio : float
397        Ratio of number of training samples to entire data set. Note:
398        if a threshold is applied, the ratio training samples may 
399        decrease depending on class balance and `class_threshold`
400        parameter if `allow_multiclass = True`.
401
402    distance_metric : str
403        The pairwise distance metric used to estimate sigma. Must
404        be one of the options used in `scipy.spatial.distance.cdist`.
405
406    kernel_type : str
407        The approximated kernel function used to calculate Zs.
408        Must be one of `'Gaussian'`, `'Laplacian'`, or `'Cauchy'`.
409
410    random_state : int
411        Integer random_state used to set the seed for 
412        reproducibilty.
413
414    allow_multiclass : bool
415        If `False`, will ensure that cell labels are binary.
416
417    class_threshold : str | int
418        Number of samples allowed in the training data for each cell
419        class in the training data. If `'median'`, the median number 
420        of cells per cell class will be the threshold for number of 
421        samples per class.
422
423    reduction: str | None
424        Choose which dimension reduction technique to perform on 
425        features within a group. 'svd' will run 
426        `sklearn.decomposition.TruncatedSVD`, 'linear' will multiply 
427        by an array of 1s down to 50 dimensions. 'pca' will replace 
428        each group values with 50 PCs from principal component 
429        analysis.
430        
431    tfidf: bool
432        Whether to calculate TFIDF transformation on peaks within 
433        groupings.
434
435    add_ones: bool
436        Allows the addition of ones for downstream functions to run 
437        single features.
438        
439    Returns
440    -------
441    adata : ad.AnnData
442        AnnData with the following attributes and keys:
443
444        `adata.X` (array_like):
445            Data matrix.
446    
447        `adata.var_names` (array_like): 
448            Feature names corresponding to `adata.X`.
449
450        `adata.obs['labels']` (array_like):
451            cell classes/phenotypes from `cell_labels`.
452
453        `adata.uns['train_indices']` (array_like):
454            Indices for training data. 
455
456        `adata.uns['test_indices']` (array_like)
457            Indices for testing data.
458
459        `adata.uns['group_dict']` (dict):
460            Grouping information.
461
462        `adata.uns['seed_obj']` (np.random._generator.Generator): 
463            Seed object with seed equal to 100 * `random_state`.
464
465        `adata.uns['D']` (int):
466            Number of dimensions to scMKL with.
467
468        `adata.uns['scale_data']` (bool):
469            Whether or not data is scaled.
470
471        `adata.uns['transform_data']` (bool):
472            Whether or not data is log1p transformed.
473
474        `adata.uns['distance_metric']` (str): 
475            Distance metric as given.
476    
477        `adata.uns['kernel_type']` (str): 
478            Kernel function as given.
479
480        `adata.uns['svd']` (bool): 
481            Whether to calculate SVD reduction.
482
483        `adata.uns['tfidf']` (bool): 
484            Whether to calculate TF-IDF per grouping.
485
486    Examples
487    --------
488    >>> data_mat = scipy.sparse.load_npz('MCF7_RNA_matrix.npz')
489    >>> gene_names = np.load('MCF7_gene_names.pkl', allow_pickle = True)
490    >>> group_dict = np.load('hallmark_genesets.pkl', 
491    >>>                      allow_pickle = True)
492    >>> 
493    >>> adata = scmkl.create_adata(X = data_mat, 
494    ...                            feature_names = gene_names, 
495    ...                            group_dict = group_dict)
496    >>> adata
497    AnnData object with n_obs × n_vars = 1000 × 4341
498    obs: 'labels'
499    uns: 'group_dict', 'seed_obj', 'scale_data', 'D', 'kernel_type', 
500    'distance_metric', 'train_indices', 'test_indices'
501    """
502    assert X.shape[1] == len(feature_names), ("Different number of features "
503                                              "in X than feature names.")
504    
505    if not allow_multiclass:
506        assert len(np.unique(cell_labels)) == 2, ("cell_labels must contain "
507                                                  "2 classes.")
508
509    kernel_options = ['gaussian', 'laplacian', 'cauchy']
510    assert kernel_type.lower() in kernel_options, ("Given kernel type not "
511                                                   "implemented. Gaussian, "
512                                                   "Laplacian, and Cauchy "
513                                                   "are the acceptable "
514                                                   "types.")
515
516    # Create adata object and add column names
517    adata = ad.AnnData(X)
518    adata.var_names = feature_names
519
520    if isinstance(obs_names, (np.ndarray)):
521        adata.obs_names = obs_names
522
523    filtered_feature_names, group_dict = filter_features(feature_names, 
524                                                          group_dict,
525                                                          add_ones)
526    
527    # Ensuring that there are common features between feature_names and groups
528    overlap_err = ("No common features between feature names and grouping "
529                   "dict. Check grouping.")
530    assert len(filtered_feature_names) > 0, overlap_err
531
532    if remove_features:
533        warnings.filterwarnings('ignore', category = ad.ImplicitModificationWarning)
534        adata = adata[:, filtered_feature_names]
535    
536    gc.collect()
537
538    # Add metadata to adata object
539    adata.uns['group_dict'] = group_dict
540    adata.uns['seed_obj'] = np.random.default_rng(100*random_state)
541    adata.uns['scale_data'] = scale_data
542    adata.uns['transform_data'] = transform_data
543    adata.uns['kernel_type'] = kernel_type
544    adata.uns['distance_metric'] = distance_metric
545    adata.uns['reduction'] = reduction if isinstance(reduction, str) else 'None'
546    adata.uns['tfidf'] = tfidf
547    adata.uns['add_ones'] = add_ones
548
549    if (split_data is None):
550        assert X.shape[0] == len(cell_labels), ("Different number of cells "
551                                                "than labels")
552        adata.obs['labels'] = cell_labels
553
554        if (allow_multiclass == False):
555            split = binary_split(cell_labels, 
556                                  seed_obj = adata.uns['seed_obj'],
557                                  train_ratio = train_ratio)
558            train_indices, test_indices = split
559
560        elif (allow_multiclass == True):
561            split = multi_class_split(cell_labels, 
562                                       seed_obj = adata.uns['seed_obj'], 
563                                       class_threshold = class_threshold,
564                                       train_ratio = train_ratio)
565            train_indices, test_indices = split
566
567        adata.uns['labeled_test'] = True
568
569    else:
570        sd_err_message = "`split_data` argument must be of type np.ndarray"
571        assert isinstance(split_data, np.ndarray), sd_err_message
572        x_eq_labs = X.shape[0] == len(cell_labels)
573        train_eq_labs = X.shape[0] == len(cell_labels)
574        assert x_eq_labs or train_eq_labs, ("Must give labels for all cells "
575                                            "or only for training cells")
576        
577        train_indices = np.where(split_data == 'train')[0]
578        test_indices = np.where(split_data == 'test')[0]
579
580        if len(cell_labels) == len(train_indices):
581
582            padded_cell_labels = np.zeros((X.shape[0])).astype('object')
583            padded_cell_labels[train_indices] = cell_labels
584            padded_cell_labels[test_indices] = 'padded_test_label'
585
586            adata.obs['labels'] = padded_cell_labels
587            adata.uns['labeled_test'] = False
588
589        elif len(cell_labels) == len(split_data):
590            adata.obs['labels'] = cell_labels
591            adata.uns['labeled_test'] = True
592
593    # Ensuring all train samples are first in adata object followed by test
594    sort_idx, train_indices, test_indices = sort_samples(train_indices, 
595                                                         test_indices)
596
597    adata = adata[sort_idx]
598
599    if not isinstance(obs_names, (np.ndarray)):
600        adata.obs = adata.obs.reset_index(drop=True)
601        adata.obs.index = adata.obs.index.astype('O')
602
603    adata.uns['train_indices'] = train_indices
604    adata.uns['test_indices'] = test_indices
605
606    adata.uns['D'] = get_optimal_d(adata, D, allow_multiclass, other_factor)
607
608    cts, cts_counts = np.unique(adata.obs['labels'], return_counts=True)
609    small_cts = cts[10 > cts_counts]
610    if small_cts.size:
611        print("WARNING: There are less than 10 cells for the "
612              "following label(s):", small_cts, 
613              "Can lead to issues running `scmkl.optimize_alpha()`.", 
614              sep='\n')
615    if not scale_data:
616        print("WARNING: Data will not be scaled. "
617              "To change this behavior, set scale_data to True")
618    if not transform_data:
619        print("WARNING: Data will not be transformed."
620              "To change this behavior, set transform_data to True")
621        
622    # Need to avoid including samples with no obervations 
623    # (must be done after feature filtering for grouping)
624    zero_var = np.array(sparse_var(adata.X, axis=1) == 0).ravel()
625    num_zero_var = np.sum(zero_var)
626    if 0 < num_zero_var:
627        print("WARNING: Some samples contain no variance across features "
628              "(likely all zeros), consider removing them and recreating "
629              "AnnData.", flush=True)
630
631    return adata

Function to create an AnnData object to carry all relevant information going forward.

Parameters
  • X (scipy.sparse.csc_matrix | np.ndarray | pd.DataFrame): A data matrix of cells by features (sparse array recommended for large datasets).
  • feature_names (np.ndarray): Array of feature names corresponding with the features in X.
  • cell_labels (np.ndarray): A numpy array of cell phenotypes corresponding with the cells in X.
  • group_dict (dict): Dictionary containing feature grouping information (i.e. {geneset1: np.array([gene_1, gene_2, ..., gene_n]), geneset2: np.array([...]), ...}.
  • obs_names (None | np.ndarray): The cell names corresponding to X to be assigned to output object .obs_names attribute.
  • scale_data (bool): If True, data matrix is log transformed and standard scaled. Default is True.
  • transform_data (bool): If True, data will be log1p transformed (recommended for counts data). Default is False.
  • split_data (None | np.ndarray): If None, data will be split stratified by cell labels. Else, is an array of precalculated train/test split corresponding to samples. Can include labels for entire dataset to benchmark performance or for only training data to classify unknown cell types (i.e. np.array(['train', 'test', ..., 'train']).
  • D (int): Number of Random Fourier Features used to calculate Z. Should be a positive integer. Higher values of D will increase classification accuracy at the cost of computation time. If set to None, will be calculated given number of samples.
  • remove_features (bool): If True, will remove features from X and feature_names not in group_dict and remove features from groupings not in feature_names.
  • train_ratio (float): Ratio of number of training samples to entire data set. Note: if a threshold is applied, the ratio training samples may decrease depending on class balance and class_threshold parameter if allow_multiclass = True.
  • distance_metric (str): The pairwise distance metric used to estimate sigma. Must be one of the options used in scipy.spatial.distance.cdist.
  • kernel_type (str): The approximated kernel function used to calculate Zs. Must be one of 'Gaussian', 'Laplacian', or 'Cauchy'.
  • random_state (int): Integer random_state used to set the seed for reproducibilty.
  • allow_multiclass (bool): If False, will ensure that cell labels are binary.
  • class_threshold (str | int): Number of samples allowed in the training data for each cell class in the training data. If 'median', the median number of cells per cell class will be the threshold for number of samples per class.
  • reduction (str | None): Choose which dimension reduction technique to perform on features within a group. 'svd' will run sklearn.decomposition.TruncatedSVD, 'linear' will multiply by an array of 1s down to 50 dimensions. 'pca' will replace each group values with 50 PCs from principal component analysis.
  • tfidf (bool): Whether to calculate TFIDF transformation on peaks within groupings.
  • add_ones (bool): Allows the addition of ones for downstream functions to run single features.
Returns
  • adata (ad.AnnData): AnnData with the following attributes and keys:

    adata.X (array_like): Data matrix.

    adata.var_names (array_like): Feature names corresponding to adata.X.

    adata.obs['labels'] (array_like): cell classes/phenotypes from cell_labels.

    adata.uns['train_indices'] (array_like): Indices for training data.

    adata.uns['test_indices'] (array_like) Indices for testing data.

    adata.uns['group_dict'] (dict): Grouping information.

    adata.uns['seed_obj'] (np.random._generator.Generator): Seed object with seed equal to 100 * random_state.

    adata.uns['D'] (int): Number of dimensions to scMKL with.

    adata.uns['scale_data'] (bool): Whether or not data is scaled.

    adata.uns['transform_data'] (bool): Whether or not data is log1p transformed.

    adata.uns['distance_metric'] (str): Distance metric as given.

    adata.uns['kernel_type'] (str): Kernel function as given.

    adata.uns['svd'] (bool): Whether to calculate SVD reduction.

    adata.uns['tfidf'] (bool): Whether to calculate TF-IDF per grouping.

Examples
>>> data_mat = scipy.sparse.load_npz('MCF7_RNA_matrix.npz')
>>> gene_names = np.load('MCF7_gene_names.pkl', allow_pickle = True)
>>> group_dict = np.load('hallmark_genesets.pkl', 
>>>                      allow_pickle = True)
>>> 
>>> adata = scmkl.create_adata(X = data_mat, 
...                            feature_names = gene_names, 
...                            group_dict = group_dict)
>>> adata
AnnData object with n_obs × n_vars = 1000 × 4341
obs: 'labels'
uns: 'group_dict', 'seed_obj', 'scale_data', 'D', 'kernel_type', 
'distance_metric', 'train_indices', 'test_indices'
def format_adata( adata: anndata._core.anndata.AnnData | str, cell_labels: numpy.ndarray | str, group_dict: dict | str, use_raw: bool = False, scale_data: bool = True, transform_data: bool = False, split_data: numpy.ndarray | None = None, D: int | None = None, remove_features: bool = True, train_ratio: float = 0.8, distance_metric: str = 'euclidean', kernel_type: str = 'Gaussian', random_state: int = 1, allow_multiclass: bool = False, class_threshold: str | int | None = None, reduction: str | None = None, tfidf: bool = False, other_factor: float = 1.5, add_ones: bool = False):
634def format_adata(adata: ad.AnnData | str, cell_labels: np.ndarray | str, 
635                 group_dict: dict | str, use_raw: bool=False, 
636                 scale_data: bool=True, transform_data: bool=False, 
637                 split_data: np.ndarray | None=None, D: int | None=None, 
638                 remove_features: bool=True, train_ratio: float=0.8, 
639                 distance_metric: str='euclidean', kernel_type: str='Gaussian', 
640                 random_state: int=1, allow_multiclass: bool = False, 
641                 class_threshold: str | int | None=None, 
642                 reduction: str | None=None, tfidf: bool=False,
643                 other_factor: float=1.5, add_ones: bool=False):
644    """
645    Function to format an `ad.AnnData` object to carry all relevant 
646    information going forward. `adata.obs_names` will be retained.
647
648    **NOTE: Information not needed for running `scmkl` will be 
649    removed.**
650
651    Parameters
652    ----------
653    adata : ad.AnnData
654        Object with data for `scmkl` to be applied to. Only requirment 
655        is that `.var_names` is correct and data matrix is in `adata.X` 
656        or `adata.raw.X`. A h5ad file can be provided as a `str` and it 
657        will be read in.
658
659    cell_labels : np.ndarray | str
660        If type `str`, the labels for `scmkl` to learn are captured 
661        from `adata.obs['cell_labels']`. Else, a `np.ndarray` of cell 
662        phenotypes corresponding with the cells in `adata.X`.
663
664    group_dict : dict | str
665        Dictionary containing feature grouping information (i.e. 
666        `{geneset1: np.array([gene_1, gene_2, ..., gene_n]), geneset2: 
667        np.array([...]), ...}`. A pickle file can be provided as a `str` 
668        and it will be read in.
669
670    obs_names : None | np.ndarray
671        The cell names corresponding to `X` to be assigned to output 
672        object `.obs_names` attribute.
673
674    use_raw : bool
675        If `False`, will use `adata.X` to create new `adata`. Else, 
676        will use `adata.raw.X`.
677
678    scale_data : bool  
679        If `True`, data matrix is log transformed and standard 
680        scaled. 
681
682    transform_data : bool
683        If `True`, data will be log1p transformed (recommended for 
684        counts data). Default is `False`. 
685        
686    split_data : None | np.ndarray
687        If `None`, data will be split stratified by cell labels. 
688        Else, is an array of precalculated train/test split 
689        corresponding to samples. Can include labels for entire
690        dataset to benchmark performance or for only training
691        data to classify unknown cell types (i.e. `np.array(['train', 
692        'test', ..., 'train'])`.
693
694    D : int 
695        Number of Random Fourier Features used to calculate Z. 
696        Should be a positive integer. Higher values of D will 
697        increase classification accuracy at the cost of computation 
698        time. If set to `None`, will be calculated given number of 
699        samples. 
700    
701    remove_features : bool
702        If `True`, will remove features from `X` and `feature_names` 
703        not in `group_dict` and remove features from groupings not in 
704        `feature_names`.
705
706    train_ratio : float
707        Ratio of number of training samples to entire data set. Note:
708        if a threshold is applied, the ratio training samples may 
709        decrease depending on class balance and `class_threshold`
710        parameter if `allow_multiclass = True`.
711
712    distance_metric : str
713        The pairwise distance metric used to estimate sigma. Must
714        be one of the options used in `scipy.spatial.distance.cdist`.
715
716    kernel_type : str
717        The approximated kernel function used to calculate Zs.
718        Must be one of `'Gaussian'`, `'Laplacian'`, or `'Cauchy'`.
719
720    random_state : int
721        Integer random_state used to set the seed for 
722        reproducibilty.
723
724    allow_multiclass : bool
725        If `False`, will ensure that cell labels are binary.
726
727    class_threshold : str | int
728        Number of samples allowed in the training data for each cell
729        class in the training data. If `'median'`, the median number 
730        of cells per cell class will be the threshold for number of 
731        samples per class.
732
733    reduction: str | None
734        Choose which dimension reduction technique to perform on 
735        features within a group. 'svd' will run 
736        `sklearn.decomposition.TruncatedSVD`, 'linear' will multiply 
737        by an array of 1s down to 50 dimensions.
738        
739    tfidf: bool
740        Whether to calculate TFIDF transformation on peaks within 
741        groupings.
742        
743    Returns
744    -------
745    adata : ad.AnnData
746        AnnData with the following attributes and keys:
747
748        `adata.X` (array_like):
749            Data matrix.
750    
751        `adata.var_names` (array_like): 
752            Feature names corresponding to `adata.X`.
753
754        `adata.obs['labels']` (array_like):
755            cell classes/phenotypes from `cell_labels`.
756
757        `adata.uns['train_indices']` (array_like):
758            Indices for training data. 
759
760        `adata.uns['test_indices']` (array_like)
761            Indices for testing data.
762
763        `adata.uns['group_dict']` (dict):
764            Grouping information.
765
766        `adata.uns['seed_obj']` (np.random._generator.Generator): 
767            Seed object with seed equal to 100 * `random_state`.
768
769        `adata.uns['D']` (int):
770            Number of dimensions to scMKL with.
771
772        `adata.uns['scale_data']` (bool):
773            Whether or not data is scaled.
774
775        `adata.uns['transform_data']` (bool):
776            Whether or not data is log1p transformed.
777
778        `adata.uns['distance_metric']` (str): 
779            Distance metric as given.
780    
781        `adata.uns['kernel_type']` (str): 
782            Kernel function as given.
783
784        `adata.uns['svd']` (bool): 
785            Whether to calculate SVD reduction.
786
787        `adata.uns['tfidf']` (bool): 
788            Whether to calculate TF-IDF per grouping.
789
790    Examples
791    --------
792    >>> adata = ad.read_h5ad('MCF7_rna.h5ad')
793    >>> group_dict = np.load('hallmark_genesets.pkl', 
794    >>>                      allow_pickle = True)
795    >>> 
796    >>> 
797    >>> # The labels in adata.obs we want to learn are 'celltypes'
798    >>> adata = scmkl.format_adata(adata, 'celltypes', 
799    ...                            group_dict)
800    >>> adata
801    AnnData object with n_obs × n_vars = 1000 × 4341
802    obs: 'labels'
803    uns: 'group_dict', 'seed_obj', 'scale_data', 'D', 'kernel_type', 
804    'distance_metric', 'train_indices', 'test_indices'
805    """
806    if str == type(adata):
807        adata = ad.read_h5ad(adata)
808
809    if str == type(group_dict):
810        group_dict = np.load(group_dict, allow_pickle=True)
811        
812    if str == type(cell_labels):
813        err_msg = f"{cell_labels} is not in `adata.obs`"
814        assert cell_labels in adata.obs.keys(), err_msg
815        cell_labels = adata.obs[cell_labels].to_numpy()
816    
817    if use_raw:
818        assert adata.raw, "`adata.raw` is empty, set `use_raw` to `False`"
819        X = adata.raw.X
820    else:
821        X = adata.X
822
823    adata = create_adata(X, adata.var_names.to_numpy().copy(), cell_labels, 
824                         group_dict, adata.obs_names.to_numpy().copy(), 
825                         scale_data, transform_data, split_data, D, remove_features, 
826                         train_ratio, distance_metric, kernel_type, 
827                         random_state, allow_multiclass, class_threshold, 
828                         reduction, tfidf, other_factor, add_ones)
829
830    return adata

Function to format an ad.AnnData object to carry all relevant information going forward. adata.obs_names will be retained.

NOTE: Information not needed for running scmkl will be removed.

Parameters
  • adata (ad.AnnData): Object with data for scmkl to be applied to. Only requirment is that .var_names is correct and data matrix is in adata.X or adata.raw.X. A h5ad file can be provided as a str and it will be read in.
  • cell_labels (np.ndarray | str): If type str, the labels for scmkl to learn are captured from adata.obs['cell_labels']. Else, a np.ndarray of cell phenotypes corresponding with the cells in adata.X.
  • group_dict (dict | str): Dictionary containing feature grouping information (i.e. {geneset1: np.array([gene_1, gene_2, ..., gene_n]), geneset2: np.array([...]), ...}. A pickle file can be provided as a str and it will be read in.
  • obs_names (None | np.ndarray): The cell names corresponding to X to be assigned to output object .obs_names attribute.
  • use_raw (bool): If False, will use adata.X to create new adata. Else, will use adata.raw.X.
  • scale_data (bool): If True, data matrix is log transformed and standard scaled.
  • transform_data (bool): If True, data will be log1p transformed (recommended for counts data). Default is False.
  • split_data (None | np.ndarray): If None, data will be split stratified by cell labels. Else, is an array of precalculated train/test split corresponding to samples. Can include labels for entire dataset to benchmark performance or for only training data to classify unknown cell types (i.e. np.array(['train', 'test', ..., 'train']).
  • D (int): Number of Random Fourier Features used to calculate Z. Should be a positive integer. Higher values of D will increase classification accuracy at the cost of computation time. If set to None, will be calculated given number of samples.
  • remove_features (bool): If True, will remove features from X and feature_names not in group_dict and remove features from groupings not in feature_names.
  • train_ratio (float): Ratio of number of training samples to entire data set. Note: if a threshold is applied, the ratio training samples may decrease depending on class balance and class_threshold parameter if allow_multiclass = True.
  • distance_metric (str): The pairwise distance metric used to estimate sigma. Must be one of the options used in scipy.spatial.distance.cdist.
  • kernel_type (str): The approximated kernel function used to calculate Zs. Must be one of 'Gaussian', 'Laplacian', or 'Cauchy'.
  • random_state (int): Integer random_state used to set the seed for reproducibilty.
  • allow_multiclass (bool): If False, will ensure that cell labels are binary.
  • class_threshold (str | int): Number of samples allowed in the training data for each cell class in the training data. If 'median', the median number of cells per cell class will be the threshold for number of samples per class.
  • reduction (str | None): Choose which dimension reduction technique to perform on features within a group. 'svd' will run sklearn.decomposition.TruncatedSVD, 'linear' will multiply by an array of 1s down to 50 dimensions.
  • tfidf (bool): Whether to calculate TFIDF transformation on peaks within groupings.
Returns
  • adata (ad.AnnData): AnnData with the following attributes and keys:

    adata.X (array_like): Data matrix.

    adata.var_names (array_like): Feature names corresponding to adata.X.

    adata.obs['labels'] (array_like): cell classes/phenotypes from cell_labels.

    adata.uns['train_indices'] (array_like): Indices for training data.

    adata.uns['test_indices'] (array_like) Indices for testing data.

    adata.uns['group_dict'] (dict): Grouping information.

    adata.uns['seed_obj'] (np.random._generator.Generator): Seed object with seed equal to 100 * random_state.

    adata.uns['D'] (int): Number of dimensions to scMKL with.

    adata.uns['scale_data'] (bool): Whether or not data is scaled.

    adata.uns['transform_data'] (bool): Whether or not data is log1p transformed.

    adata.uns['distance_metric'] (str): Distance metric as given.

    adata.uns['kernel_type'] (str): Kernel function as given.

    adata.uns['svd'] (bool): Whether to calculate SVD reduction.

    adata.uns['tfidf'] (bool): Whether to calculate TF-IDF per grouping.

Examples
>>> adata = ad.read_h5ad('MCF7_rna.h5ad')
>>> group_dict = np.load('hallmark_genesets.pkl', 
>>>                      allow_pickle = True)
>>> 
>>> 
>>> # The labels in adata.obs we want to learn are 'celltypes'
>>> adata = scmkl.format_adata(adata, 'celltypes', 
...                            group_dict)
>>> adata
AnnData object with n_obs × n_vars = 1000 × 4341
obs: 'labels'
uns: 'group_dict', 'seed_obj', 'scale_data', 'D', 'kernel_type', 
'distance_metric', 'train_indices', 'test_indices'