#!/usr/bin/env python # coding: utf-8 # # Consensus Non-negative Matrix factorization (cNMF) # # cNMF is an analysis pipeline for inferring gene expression programs from single-cell RNA-Seq (scRNA-Seq) data. # # It takes a count matrix (N cells X G genes) as input and produces a (K x G) matrix of gene expression programs (GEPs) and a (N x K) matrix specifying the usage of each program for each cell in the data. You can read more about the method in the [github](https://github.com/dylkot/cNMF) and check out examples on dentategyrus. # In[1]: import scanpy as sc import omicverse as ov ov.plot_set() # ## Loading dataset # # Here, we use the dentategyrus dataset as an example for cNMF. # In[2]: import scvelo as scv adata=scv.datasets.dentategyrus() # In[3]: get_ipython().run_cell_magic('time', '', "adata=ov.pp.preprocess(adata,mode='shiftlog|pearson',n_HVGs=2000,)\nadata\n") # In[23]: ov.pp.scale(adata) ov.pp.pca(adata) # In[4]: import matplotlib.pyplot as plt from matplotlib import patheffects fig, ax = plt.subplots(figsize=(4,4)) ov.pl.embedding( adata, basis="X_umap", color=['clusters'], frameon='small', title="Celltypes", #legend_loc='on data', legend_fontsize=14, legend_fontoutline=2, #size=10, ax=ax, #legend_loc=True, add_outline=False, #add_outline=True, outline_color='black', outline_width=1, show=False, ) # ## Initialize and Training model # In[5]: 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_dg/cNMF', name='dg_cNMF') # In[6]: ## 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=2) # In[7]: cnmf_obj.combine(skip_missing_files=True) # ## Compute the stability and error at each choice of K to see if a clear choice jumps out. # # Please note that the maximum stability solution is not always the best choice depending on the application. However it is often a good starting point even if you have to investigate several choices of K # In[8]: cnmf_obj.k_selection_plot(close_fig=False) # In this range, K=7 gave the most stable solution so we will begin by looking at that. # # The next step computes the consensus solution for a given choice of K. We first run it without any outlier filtering to see what that looks like. Setting the density threshold to anything >= 2.00 (the maximum possible distance between two unit vectors) ensures that nothing will be filtered. # # Then we run the consensus with a filter for outliers determined based on inspecting the histogram of distances between components and their nearest neighbors # In[9]: selected_K = 7 density_threshold = 2.00 # In[10]: cnmf_obj.consensus(k=selected_K, density_threshold=density_threshold, show_clustering=True, close_clustergram_fig=False) # The above consensus plot shows that there is a substantial degree of concordance between the replicates with a few outliers. An outlier threshold of 0.1 seems appropriate # In[11]: density_threshold = 0.10 # In[12]: cnmf_obj.consensus(k=selected_K, density_threshold=density_threshold, show_clustering=True, close_clustergram_fig=False) # ## Visualization the result # In[13]: import seaborn as sns import matplotlib.pyplot as plt from matplotlib import patheffects from matplotlib import gridspec import matplotlib.pyplot as plt width_ratios = [0.2, 4, 0.5, 10, 1] height_ratios = [0.2, 4] fig = plt.figure(figsize=(sum(width_ratios), sum(height_ratios))) gs = gridspec.GridSpec(len(height_ratios), len(width_ratios), fig, 0.01, 0.01, 0.98, 0.98, height_ratios=height_ratios, width_ratios=width_ratios, wspace=0, hspace=0) D = cnmf_obj.topic_dist[cnmf_obj.spectra_order, :][:, cnmf_obj.spectra_order] dist_ax = fig.add_subplot(gs[1,1], xscale='linear', yscale='linear', xticks=[], yticks=[],xlabel='', ylabel='', frameon=True) dist_im = dist_ax.imshow(D, interpolation='none', cmap='viridis', aspect='auto', rasterized=True) left_ax = fig.add_subplot(gs[1,0], xscale='linear', yscale='linear', xticks=[], yticks=[], xlabel='', ylabel='', frameon=True) left_ax.imshow(cnmf_obj.kmeans_cluster_labels.values[cnmf_obj.spectra_order].reshape(-1, 1), interpolation='none', cmap='Spectral', aspect='auto', rasterized=True) top_ax = fig.add_subplot(gs[0,1], xscale='linear', yscale='linear', xticks=[], yticks=[], xlabel='', ylabel='', frameon=True) top_ax.imshow(cnmf_obj.kmeans_cluster_labels.values[cnmf_obj.spectra_order].reshape(1, -1), interpolation='none', cmap='Spectral', aspect='auto', rasterized=True) cbar_gs = gridspec.GridSpecFromSubplotSpec(3, 3, subplot_spec=gs[1, 2], wspace=0, hspace=0) cbar_ax = fig.add_subplot(cbar_gs[1,2], xscale='linear', yscale='linear', xlabel='', ylabel='', frameon=True, title='Euclidean\nDistance') cbar_ax.set_title('Euclidean\nDistance',fontsize=12) vmin = D.min().min() vmax = D.max().max() fig.colorbar(dist_im, cax=cbar_ax, ticks=np.linspace(vmin, vmax, 3), ) cbar_ax.set_yticklabels(cbar_ax.get_yticklabels(),fontsize=12) # In[14]: density_filter = cnmf_obj.local_density.iloc[:, 0] < density_threshold fig, hist_ax = plt.subplots(figsize=(4,4)) #hist_ax = fig.add_subplot(hist_gs[0,0], xscale='linear', yscale='linear', # xlabel='', ylabel='', frameon=True, title='Local density histogram') hist_ax.hist(cnmf_obj.local_density.values, bins=np.linspace(0, 1, 50)) hist_ax.yaxis.tick_right() xlim = hist_ax.get_xlim() ylim = hist_ax.get_ylim() if density_threshold < xlim[1]: hist_ax.axvline(density_threshold, linestyle='--', color='k') hist_ax.text(density_threshold + 0.02, ylim[1] * 0.95, 'filtering\nthreshold\n\n', va='top') hist_ax.set_xlim(xlim) hist_ax.set_xlabel('Mean distance to k nearest neighbors\n\n%d/%d (%.0f%%) spectra above threshold\nwere removed prior to clustering'%(sum(~density_filter), len(density_filter), 100*(~density_filter).mean())) hist_ax.set_title('Local density histogram') # ## Explode the cNMF result # # We can load the results for a cNMF run with a given K and density filtering threshold like below # In[15]: result_dict = cnmf_obj.load_results(K=selected_K, density_threshold=density_threshold) # In[16]: result_dict['usage_norm'].head() # In[17]: result_dict['gep_scores'].head() # In[18]: result_dict['gep_tpm'].head() # In[19]: result_dict['top_genes'].head() # We can extract cell classes directly based on the highest cNMF in each cell, but this has the disadvantage that it will lead to mixed cell classes if the heterogeneity of our data is not as strong as it should be. # In[20]: cnmf_obj.get_results(adata,result_dict) # In[21]: 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[24]: ov.pl.embedding( adata, basis="X_umap", color=['cNMF_cluster'], 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, ) # Here we are, proposing another idea of categorisation. We use cells with cNMF greater than 0.5 as a primitive class, and then train a random forest classification model, and then use the random forest classification model to classify cells with cNMF less than 0.5 to get a more accurate # In[25]: cnmf_obj.get_results_rfc(adata,result_dict, use_rep='scaled|original|X_pca', cNMF_threshold=0.5) # In[27]: 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, ) # In[25]: plot_genes=[] for i in result_dict['top_genes'].columns: plot_genes+=result_dict['top_genes'][i][:3].values.reshape(-1).tolist() # In[26]: sc.pl.dotplot(adata,plot_genes, "cNMF_cluster", dendrogram=False,standard_scale='var',)