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
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.
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.AnnDataobject. - 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_thresholdparameter. - 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.
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.
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_indicesin.unskeys andlabelsin.obskeys.
Returns
- median_size (int): The median size of training plus testing samples across cell types.
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
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_indicesin.unskeys andlabelsin.obskeys. - D (int | None):
The D provided as an
intorNoneif optimal D should be calculated. - allow_multiclass (bool):
Should be
Falseif labels are binary. Else, should beTrueindicating there are more than two classes.
Returns
- d (int): Either the input or calculated optimal d for the experiment.
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.
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
Xto be assigned to output object.obs_namesattribute. - scale_data (bool):
If
True, data matrix is log transformed and standard scaled. Default isTrue. - transform_data (bool):
If
True, data will be log1p transformed (recommended for counts data). Default isFalse. - 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 fromXandfeature_namesnot ingroup_dictand remove features from groupings not infeature_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_thresholdparameter ifallow_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 toadata.X.adata.obs['labels'](array_like): cell classes/phenotypes fromcell_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'
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
scmklto be applied to. Only requirment is that.var_namesis correct and data matrix is inadata.Xoradata.raw.X. A h5ad file can be provided as astrand it will be read in. - cell_labels (np.ndarray | str):
If type
str, the labels forscmklto learn are captured fromadata.obs['cell_labels']. Else, anp.ndarrayof cell phenotypes corresponding with the cells inadata.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 astrand it will be read in. - obs_names (None | np.ndarray):
The cell names corresponding to
Xto be assigned to output object.obs_namesattribute. - use_raw (bool):
If
False, will useadata.Xto create newadata. Else, will useadata.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 isFalse. - 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 fromXandfeature_namesnot ingroup_dictand remove features from groupings not infeature_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_thresholdparameter ifallow_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 toadata.X.adata.obs['labels'](array_like): cell classes/phenotypes fromcell_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'