#!/usr/bin/env python # coding: utf-8 # # Clustering space # # In this tutorial, we will explore how to run the Supervised clustering, unsupervised clustering, and amortized Latent Dirichlet Allocation (LDA) model implementation in `omicverse` with `GaussianMixture`,`Leiden/Louvain` and `MiRA`. # # In scRNA-seq data analysis, we describe cellular structure in our dataset with finding cell identities that relate to known cell states or cell cycle stages. This process is usually called cell identity annotation. For this purpose, we structure cells into clusters to infer the identity of similar cells. Clustering itself is a common unsupervised machine learning problem. # # LDA is a topic modelling method first introduced in the natural language processing field. By treating each cell as a document and each gene expression count as a word, we can carry over the method to the single-cell biology field. # # Below, we will train the model over a dataset, plot the topics over a UMAP of the reference set, and inspect the topics for characteristic gene sets. # # ## Preprocess data # # As an example, we apply differential kinetic analysis to dentate gyrus neurogenesis, which comprises multiple heterogeneous subpopulations. # # Colab_Reproducibility:https://colab.research.google.com/drive/1d_szq-y-g7O0C5rJgK22XC7uWTUNrYpK?usp=sharing # In[1]: import omicverse as ov import scanpy as sc import scvelo as scv ov.plot_set() # In[2]: import scvelo as scv adata=scv.datasets.dentategyrus() adata # In[3]: adata=ov.pp.preprocess(adata,mode='shiftlog|pearson',n_HVGs=3000,) adata.raw = adata adata = adata[:, adata.var.highly_variable_features] ov.pp.scale(adata) ov.pp.pca(adata,layer='scaled',n_pcs=50) # Let us inspect the contribution of single PCs to the total variance in the data. This gives us information about how many PCs we should consider in order to compute the neighborhood relations of cells. In our experience, often a rough estimate of the number of PCs does fine. # In[4]: ov.utils.plot_pca_variance_ratio(adata) # ## Unsupervised clustering # # The Leiden algorithm is as an improved version of the Louvain algorithm which outperformed other clustering methods for single-cell RNA-seq data analysis ([Du et al., 2018, Freytag et al., 2018, Weber and Robinson, 2016]). Since the Louvain algorithm is no longer maintained, using Leiden instead is preferred. # # We, therefore, propose to use the Leiden algorithm[Traag et al., 2019] on single-cell k-nearest-neighbour (KNN) graphs to cluster single-cell datasets. # # Leiden creates clusters by taking into account the number of links between cells in a cluster versus the overall expected number of links in the dataset. # # Here, we set `method='leiden'` to cluster the cells using `Leiden` # # In[5]: sc.pp.neighbors(adata, n_neighbors=15, n_pcs=50, use_rep='scaled|original|X_pca') ov.utils.cluster(adata,method='leiden',resolution=1) # In[6]: ov.utils.embedding(adata,basis='X_umap', color=['clusters','leiden'], frameon='small',wspace=0.5) # We can also set `method='louvain'` to cluster the cells using `Louvain` # In[7]: sc.pp.neighbors(adata, n_neighbors=15, n_pcs=50, use_rep='scaled|original|X_pca') ov.utils.cluster(adata,method='louvain',resolution=1) # In[8]: ov.utils.embedding(adata,basis='X_umap', color=['clusters','louvain'], frameon='small',wspace=0.5) # ## Supervised clustering # # In addition to clustering using unsupervised clustering methods, we can also try supervised clustering methods, such as Gaussian mixture model clustering, which is a supervised clustering algorithm that works better in machine learning # # Gaussian mixture models can be used to cluster unlabeled data in much the same way as k-means. There are, however, a couple of advantages to using Gaussian mixture models over k-means. # # First and foremost, k-means does not account for variance. By variance, we are referring to the width of the bell shape curve. # # The second difference between k-means and Gaussian mixture models is that the former performs hard classification whereas the latter performs soft classification. In other words, k-means tells us what data point belong to which cluster but won’t provide us with the probabilities that a given data point belongs to each of the possible clusters. # # Here, we set `method='GMM'` to cluster the cells using `GaussianMixture`,`n_components` means the PCs to be used in clustering, `covariance_type` means the Gaussian Mixture Models (`diagonal`, `spherical`, `tied` and `full` covariance matrices supported). More arguments could found in [`sklearn.mixture.GaussianMixture`](http://scikit-learn.org/stable/modules/generated/sklearn.mixture.GaussianMixture.html) # # In[9]: ov.utils.cluster(adata,use_rep='scaled|original|X_pca', method='GMM',n_components=21, covariance_type='full',tol=1e-9, max_iter=1000, ) # In[10]: ov.utils.embedding(adata,basis='X_umap', color=['clusters','gmm_cluster'], frameon='small',wspace=0.5) # ## Latent Dirichlet Allocation (LDA) model implementation # # Topic models, like Latent Dirichlet Allocation (LDA), have traditionally been used to decompose a corpus of text into topics - or themes - composed of words that often appear together in documents. Documents, in turn, are modeled as a mixture of topics based on the words they contain. # # MIRA extends these ideas to single-cell genomics data, where topics are groups of genes that are co-expressed or cis-regulatory elements that are co-accessible, and cells are a mixture of these regulatory modules. # # Here, we used `ov.utils.LDA_topic` to construct the model of MiRA. # # Particularly, and at a minimum, we must tell the model # # - feature_type: what type of features we are working with (either “expression” or “accessibility”) # - highly_variable_key: which .var key to find our highly variable genes # - counts_layer: which layer to get the raw counts from. # - categorical_covariates, continuous_covariates: Technical variables influencing the generative process of the data. For example, a categorical technical factor may be the cells’ batch of origin, as shown here. A continous technical factor might be % of mitchondrial reads. For unbatched data, ignore these parameters. # - learning_rate: for larger datasets, the default of 1e-3, 0.1 usually works well. # In[11]: LDA_obj=ov.utils.LDA_topic(adata,feature_type='expression', highly_variable_key='highly_variable_features', layers='counts',batch_key=None,learning_rate=1e-3) # This method works by instantiating a special version of the CODAL model with far too many topics, which are gradually pruned if that topic is not needed to describe the data. The function returns the maximum contribution of each topic to any cell in the dataset. The predicted number of topics is given by the elbo of the maximum contribution curve, minus 1. A rule of thumb is that the last valid topic to include in the model is followed by a drop-off, after which all subsequent topics hover between 0.-0.05 maximum contributions. # # We set `NUM_TOPICS` to six to try. # In[12]: LDA_obj.plot_topic_contributions(6) # We can observe that there are 13 TOPICs to be above the threshold line, so we set the new NUM_TOPIC to 12 # In[13]: LDA_obj.predicted(13) # One can plot the distribution of topics across cells to see how the latent space reflects changes in cell state: # In[14]: ov.plot_set() ov.utils.embedding(adata, basis='X_umap',color = LDA_obj.model.topic_cols, cmap='BuPu', ncols=4, add_outline=True, frameon='small',) # In[15]: ov.utils.embedding(adata,basis='X_umap', color=['clusters','LDA_cluster'], frameon='small',wspace=0.5) # Here we are, proposing another idea of categorisation. We use cells with LDA greater than 0.4 as a primitive class, and then train a random forest classification model, and then use the random forest classification model to classify cells with LDA less than 0.5 to get a more accurate # In[20]: LDA_obj.get_results_rfc(adata,use_rep='scaled|original|X_pca', LDA_threshold=0.4,num_topics=13) # In[21]: ov.utils.embedding(adata,basis='X_umap', color=['LDA_cluster_rfc','LDA_cluster_clf'], frameon='small',wspace=0.5) # ## cNMF # # More detail could be found in https://starlitnightly.github.io/omicverse/Tutorials-single/t_cnmf/ # In[32]: adata.X.toarray() # In[ ]: import numpy as np ## Initialize the cnmf object that will be used to run analyses cnmf_obj = ov.single.cNMF(adata,components=np.arange(5,11), n_iter=20, seed=14, num_highvar_genes=2000, output_dir='example_dg1/cNMF', name='dg_cNMF') ## Specify that the jobs are being distributed over a single worker (total_workers=1) and then launch that worker cnmf_obj.factorize(worker_i=0, total_workers=4) cnmf_obj.combine(skip_missing_files=True) cnmf_obj.k_selection_plot(close_fig=False) # In[35]: selected_K = 7 density_threshold = 2.00 cnmf_obj.consensus(k=selected_K, density_threshold=density_threshold, show_clustering=True, close_clustergram_fig=False) result_dict = cnmf_obj.load_results(K=selected_K, density_threshold=density_threshold) cnmf_obj.get_results(adata,result_dict) # In[36]: ov.pl.embedding(adata, basis='X_umap',color=result_dict['usage_norm'].columns, use_raw=False, ncols=3, vmin=0, vmax=1,frameon='small') # In[40]: cnmf_obj.get_results_rfc(adata,result_dict, use_rep='scaled|original|X_pca', cNMF_threshold=0.5) # In[41]: ov.pl.embedding( adata, basis="X_umap", color=['cNMF_cluster_rfc','cNMF_cluster_clf'], frameon='small', #title="Celltypes", #legend_loc='on data', legend_fontsize=14, legend_fontoutline=2, #size=10, #legend_loc=True, add_outline=False, #add_outline=True, outline_color='black', outline_width=1, show=False, ) # ## Evaluation the clustering space # # Rand index adjusted for chance. The Rand Index computes a similarity measure between two clusterings by considering all pairs of samples and counting pairs that are assigned in the same or different clusters in the predicted and true clusterings. # In[42]: from sklearn.metrics.cluster import adjusted_rand_score ARI = adjusted_rand_score(adata.obs['clusters'], adata.obs['leiden']) print('Leiden, Adjusted rand index = %.2f' %ARI) ARI = adjusted_rand_score(adata.obs['clusters'], adata.obs['louvain']) print('Louvain, Adjusted rand index = %.2f' %ARI) ARI = adjusted_rand_score(adata.obs['clusters'], adata.obs['gmm_cluster']) print('GMM, Adjusted rand index = %.2f' %ARI) ARI = adjusted_rand_score(adata.obs['clusters'], adata.obs['LDA_cluster']) print('LDA, Adjusted rand index = %.2f' %ARI) ARI = adjusted_rand_score(adata.obs['clusters'], adata.obs['LDA_cluster_rfc']) print('LDA_rfc, Adjusted rand index = %.2f' %ARI) ARI = adjusted_rand_score(adata.obs['clusters'], adata.obs['LDA_cluster_clf']) print('LDA_clf, Adjusted rand index = %.2f' %ARI) ARI = adjusted_rand_score(adata.obs['clusters'], adata.obs['cNMF_cluster_rfc']) print('cNMF_rfc, Adjusted rand index = %.2f' %ARI) ARI = adjusted_rand_score(adata.obs['clusters'], adata.obs['cNMF_cluster_clf']) print('cNMF_clf, Adjusted rand index = %.2f' %ARI) # We can find that the LDA topic model is the most effective among the above clustering algorithms, but it also takes the longest computation time and requires GPU resources. We notice that the Gaussian mixture model is second only to the LDA topic model. The GMM will be a great choice in omicverse's future clustering algorithms for spatial transcriptomics.