單細胞RNA-seq生信分析全流程——第四篇:質控

4. 質控Quality Control

4.1 前言

選擇適合數據且不會過度校正或消除生物效應的預處理方法至關重要。
由于新的測序技術以及捕獲的細胞、測量的基因和識別的細胞群數量的不斷增加,用于分析單細胞RNA測序數據的工具集正在快速發展。其中許多工具專用于預處理,旨在解決以下分析步驟:雙聯體doublet檢測、質量控制、歸一化、特征feature選擇和降維。在本部分中選擇的工具,可能嚴重影響數據的下游分析和解釋。例如,如果在質量控制過程中過濾掉過多的細胞,您可能會丟失稀有的細胞亞群,并錯過對有趣的細胞生物學的深入了解。然而,如果指定的標準過于寬松,并且沒有在預處理步驟中排除質量較差的細胞,則可能會很難注釋細胞。因此,選擇能夠提供最佳并被證明在下游任務方面優于其他工具的方法非常重要。
本教程的起點是單細胞數據,已按照前面所述的部分進行處理。將數據比對以獲得分子計數矩陣,即所謂的計數矩陣或讀數矩陣。計數矩陣和讀數矩陣之間的差異取決于單細胞文庫構建方案中是否包含唯一分子標識符(UMI)。讀數和計數矩陣的維數為條形碼數 x 轉錄本數。值得注意的是,這里使用術語“條形碼”而不是“細胞”,因為條形碼可能錯誤地標記了多個細胞(雙聯體)或可能沒有標記任何細胞(空滴/孔)。我們將在“雙聯體檢測”部分對此進行詳細說明。

4.2 環境配置和數據

我們使用為2021年NeurIPS會議上的單細胞數據集成生成10x Multiome數據集[Luecken et al., 2021]。該數據集捕獲了在四個不同地點測量的12名健康人類捐贈者的骨髓單核細胞的單細胞多組學數據,以獲得嵌套批次效應。在本教程中,我們將使用一批上述數據集(供體8的樣本4)來展示scRNA-seq數據預處理的實戰流程。
第一步,我們首先使用scanpy加載數據集。

import numpy as np
import scanpy as sc
import seaborn as sns
from scipy.stats import median_abs_deviation

sc.settings.verbosity = 0
sc.settings.set_figure_params(
    dpi=80,
    facecolor="white",
    frameon=False,
)
adata = sc.read_10x_h5(
    filename="filtered_feature_bc_matrix.h5",
    backup_url="https://figshare.com/ndownloader/files/39546196",
)
adata

輸出結果:

Variable names are not unique. To make them unique, call `.var_names_make_unique`.
Variable names are not unique. To make them unique, call `.var_names_make_unique`.
AnnData object with n_obs × n_vars = 16934 × 36601
    var: 'gene_ids', 'feature_types', 'genome'

讀取數據后,scanpy會顯示一條警告,指出并非所有變量名稱都是唯一的。這表明某些變量(=基因)出現多次,這可能會導致下游分析任務出現錯誤或意外行為。我們執行建議的函數var_names_make_unique(),該函數通過將數字字符串附加到每個重復的索引元素:“1”、“2”等來使變量名稱唯一。

adata.var_names_make_unique()
adata

輸出結果:

AnnData object with n_obs × n_vars = 16934 × 36601
    var: 'gene_ids', 'feature_types', 'genome'

數據集的形狀為n_obs 16,934 x n_vars 36,601。這轉化為barcodes x number of transcripts。我們還檢查.var中有關gene_ids (Ensembl Id)、feature_types和基因組的更多信息。
大多數后續分析任務假設數據集中的每個觀測值代表來自一個完整單細胞的測量值。在某些情況下,低質量細胞、無細胞RNA或雙聯體的污染可能會違反這一假設。本教程將指導您如何糾正和消除這種違規行為并獲得高質量的數據集。

4.3 過濾低質量的讀數reads

質量控制的第一步是從數據集中刪除低質量的讀數。當細胞檢測到的基因數量較少、計數深度較低且線粒體計數較高時,這表明細胞膜可能破裂,細胞正在死亡。由于這些細胞通常不是我們分析的主要目標,并且可能會扭曲我們的下游分析,因此我們在質量控制過程中將其去除。為了識別它們,我們定義了細胞質量控制(QC)閾值。細胞質控通常對以下三個質控協變量進行:

  • 1.每個條形碼的計數數量(計數深度)
  • 2.每個條形碼的基因數量
  • 3.每個條形碼的線粒體基因計數分數

在細胞QC中,這些協變量通過閾值過濾,因為它們可能對應于死亡細胞。正如所指出的,它們可能反映了膜破裂的細胞,其細胞質mRNA已泄漏,因此只有線粒體中的mRNA仍然存在。這些細胞可能會顯示出較低的計數深度、很少檢測到的基因以及較高比例的線粒體讀數。然而,共同考慮三個QC協變量至關重要,否則可能會導致細胞信號的誤解。例如,線粒體計數相對較高的細胞可能參與呼吸過程,不應被過濾掉。計數低或高的細胞可能對應于靜止細胞群或尺寸較大的細胞。因此,優先考慮多個協變量再做出閾值決策。一般來說,建議排除較少的細胞,并盡可能避免過濾掉活細胞群或小亞群。
僅對少數或小型數據集進行QC通常以手動方式執行,方法是查看不同QC協變量的分布并識別隨后將被過濾的異常值。然而,隨著數據集規模的增長,這項任務變得越來越耗時,可能值得考慮通過MAD(median absolute deviations中值絕對偏差)進行自動閾值處理。如果細胞相差5 MADs,我們將其標記為異常值,這是一種相對寬松的過濾策略。我們想強調的是,在細胞注釋后重新評估過濾可能是合理的。
在QC中,第一步是計算QC協變量或指標。我們使用scanpy函數sc.pp.calculate_qc_metrics來計算這些值,該函數還可以計算特定基因群體的計數比例。因此,我們定義了線粒體、核糖體和血紅蛋白基因。值得注意的是,線粒體計數用前綴“mt-”或“MT-”注釋,具體取決于數據集中考慮的物種。如前所述,本篇中使用的數據集是人骨髓,因此線粒體計數用前綴“MT-”注釋。對于鼠標數據集,前綴通常是小寫,即“mt-”。

# mitochondrial genes
adata.var["mt"] = adata.var_names.str.startswith("MT-")
# ribosomal genes
adata.var["ribo"] = adata.var_names.str.startswith(("RPS", "RPL"))
# hemoglobin genes.
adata.var["hb"] = adata.var_names.str.contains(("^HB[^(P)]"))

現在我們可以用scanpy計算對應的QC指標。

sc.pp.calculate_qc_metrics(
    adata, qc_vars=["mt", "ribo", "hb"], inplace=True, percent_top=[20], log1p=True
)
adata

輸出結果:

AnnData object with n_obs × n_vars = 16934 × 36601
    obs: 'n_genes_by_counts', 'log1p_n_genes_by_counts', 'total_counts', 'log1p_total_counts', 'pct_counts_in_top_20_genes', 'total_counts_mt', 'log1p_total_counts_mt', 'pct_counts_mt', 'total_counts_ribo', 'log1p_total_counts_ribo', 'pct_counts_ribo', 'total_counts_hb', 'log1p_total_counts_hb', 'pct_counts_hb'
    var: 'gene_ids', 'feature_types', 'genome', 'mt', 'ribo', 'hb', 'n_cells_by_counts', 'mean_counts', 'log1p_mean_counts', 'pct_dropout_by_counts', 'total_counts', 'log1p_total_counts'

正如我們所看到的,該函數向.var.obs添加了幾個附加列。我們想在這里重點介紹其中的一些,有關不同指標的更多信息可以在scanpy文檔中找到:

  • .obs中的n_genes_by_counts是細胞中計數為正的基因數量。
  • total_counts是細胞的計數總數,這也可能稱為庫大小。
  • pct_counts_mt是線粒體細胞總數的比例。
    現在,我們繪制每個樣本的三個QC協變量n_genes_by_countstotal_countspct_counts_mt,以評估各個細胞的捕獲情況。
p1 = sns.displot(adata.obs["total_counts"], bins=100, kde=False)
# sc.pl.violin(adata, 'total_counts')
p2 = sc.pl.violin(adata, "pct_counts_mt")
p3 = sc.pl.scatter(adata, "total_counts", "n_genes_by_counts", color="pct_counts_mt")

輸出結果:

... storing 'feature_types' as categorical
... storing 'genome' as categorical

這些圖表明,一些讀數具有相對較高百分比的線粒體計數,這通常與細胞裂解相關。但由于每個細胞的計數數量足夠高,并且大多數細胞的線粒體讀數百分比低于 20%,我們仍然可以繼續下一步分析數據。 基于這些圖,現在還可以定義用于過濾細胞的手動閾值。在這里,我們將展示基于MAD的自動閾值和過濾的QC。
首先,我們定義一個metric函數,即.obs中的一列和過濾策略中仍然允許的MAD(nmad) 數量。

def is_outlier(adata, metric: str, nmads: int):
    M = adata.obs[metric]
    outlier = (M < np.median(M) - nmads * median_abs_deviation(M)) | (
        np.median(M) + nmads * median_abs_deviation(M) < M
    )
    return outlier

我們現在將此函數應用于log1p_total_countslog1p_n_genes_by_countspct_counts_in_top_20_genesQC協變量,每個協變量的閾值為5 MADs。

adata.obs["outlier"] = (
    is_outlier(adata, "log1p_total_counts", 5)
    | is_outlier(adata, "log1p_n_genes_by_counts", 5)
    | is_outlier(adata, "pct_counts_in_top_20_genes", 5)
)
adata.obs.outlier.value_counts()

輸出結果:

False    16065
True       869
Name: outlier, dtype: int64

pct_counts_Mt使用3 MADs進行過濾。此外,線粒體計數百分比超過8%的細胞也會被濾除。

adata.obs["mt_outlier"] = is_outlier(adata, "pct_counts_mt", 3) | (
    adata.obs["pct_counts_mt"] > 8
)
adata.obs.mt_outlier.value_counts()

輸出結果:

False    15240
True      1694
Name: mt_outlier, dtype: int64

現在,我們根據這兩個附加列來過濾AnnData對象。

print(f"Total number of cells: {adata.n_obs}")
adata = adata[(~adata.obs.outlier) & (~adata.obs.mt_outlier)].copy()

print(f"Number of cells after filtering of low quality cells: {adata.n_obs}")

輸出結果:

Total number of cells: 16934
Number of cells after filtering of low quality cells: 14814
p1 = sc.pl.scatter(adata, "total_counts", "n_genes_by_counts", color="pct_counts_mt")

4.4 環境RNA校正

對于基于液滴的單細胞RNA-seq實驗,稀釋液中存在一定量的背景mRNA,這些mRNA會與細胞一起分布到液滴中并與其一起測序。其最終效果是產生背景污染,該背景污染代表的表達不是來自液滴內包含的細胞,而是來自包含細胞的溶液。
基于液滴的scRNA-seq生成跨多個細胞的基因的唯一分子標識符 (UMI) 計數,旨在識別每個基因和每個細胞的分子數量。它假設每個液滴都含有來自單個細胞的mRNA。 雙聯體、空滴和無細胞RNA可能違反這一假設。無細胞mRNA分子代表稀釋液中存在的背景mRNA。這些分子沿著液滴分布,并與它們一起測序。輸入溶液中無細胞mRNA的這種污染通常稱為“湯”,是由細胞裂解產生的。

無細胞mRNA分子(也稱為環境RNA)可能會混淆觀察到的計數數量,并可被視為背景污染。糾正基于液滴的scRNA-seq數據集以獲得無細胞mRNA非常重要,因為它可能會扭曲我們下游分析中數據的解釋。一般來說,每個輸入溶液的湯都不同,并且取決于數據集中各個細胞的表達模式。去除環境mRNA的方法(例如SoupX和 DecontX)旨在估計湯的成分,并根據湯的表達校正計數表。
第一步,SoupX計算湯的profile。它根據未過濾的Cellranger矩陣給出的空液滴估計環境mRNA表達譜。接下來,SoupX估計細胞特定的污染分數。最后,它根據環境mRNA表達譜和估計的污染來校正表達矩陣。
SoupX的輸出是修改后的計數矩陣,可用于任何下游分析工具。
我們現在加載運行SoupX所需的python和R包。

import anndata2ri
import logging

import rpy2.rinterface_lib.callbacks as rcb
import rpy2.robjects as ro

rcb.logger.setLevel(logging.ERROR)
ro.pandas2ri.activate()
anndata2ri.activate()

%load_ext rpy2.ipython
%%R
library(SoupX)

SoupX可以在沒有聚類信息的情況下運行,但是如果提供基本聚類,結果會更好。SoupX可以與cellranger生成的默認cluster一起使用,也可以通過手動定義cluster來使用。我們將在本篇中展示后者,因為SoupX的結果對所使用的聚類并不強烈敏感。
現在,我們創建AnnData對象的副本,對其進行標準化,降低其維度,并在處理后的副本上計算默認leiden簇。后續章節將更詳細地介紹聚類。現在我們只需要知道leiden聚類為我們提供了數據集中細胞的分區(社區)。我們將獲得的簇保存為soupx_groups并刪除AnnData對象的副本以節省內存。
首先,我們生成AnnData對象的副本,對其進行標準化和log1p轉換。此時我們使用簡單的移位對數歸一化。

adata_pp = adata.copy()
sc.pp.normalize_per_cell(adata_pp)
sc.pp.log1p(adata_pp)

接下來,我們計算數據的主成分以獲得較低維的形式。然后使用該形式來生成數據的鄰域圖并在KNN 圖上運行leiden聚類。我們將簇作為soupx_groups添加到.obs并將它們保存為向量。

sc.pp.pca(adata_pp)
sc.pp.neighbors(adata_pp)
sc.tl.leiden(adata_pp, key_added="soupx_groups")

# Preprocess variables for SoupX
soupx_groups = adata_pp.obs["soupx_groups"]

現在,我們可以刪除AnnData對象的副本,因為我們生成了可在soupX中使用的簇向量。

del adata_pp

接下來,我們保存細胞名稱、基因名稱和過濾后的cellranger輸出的數據矩陣。SoupX需要features x barcodes的矩陣,因此我們必須轉置.X

cells = adata.obs_names
genes = adata.var_names
data = adata.X.T

SoupX還需要細胞矩陣的原始基因,該矩陣在cellranger輸出中通常稱為raw_feature_bc_matrix.h5。我們像之前一樣使用scanpy加載filtered_feature_bc_matrix.h5并在對象上運行.var_names_make_unique()并轉置相應的.X

adata_raw = sc.read_10x_h5(
    filename="raw_feature_bc_matrix.h5",
    backup_url="https://figshare.com/ndownloader/files/39546217",
)
adata_raw.var_names_make_unique()
data_tod = adata_raw.X.T

輸出結果:

Variable names are not unique. To make them unique, call `.var_names_make_unique`.
Variable names are not unique. To make them unique, call `.var_names_make_unique`.
del adata_raw

現在,我們已準備好運行SoupX的一切。輸入是經過過濾的條形碼 x 細胞的cellranger矩陣、來自cellranger的barcodes x droplets液滴原始表、基因和細胞名稱以及通過簡單的leiden聚類獲得的簇。輸出將是校正后的計數矩陣。
我們首先從液滴表和細胞表構建一個SoupChannel。接下來,我們將元數據添加到SoupChannel對象中,該對象可以是data.frame形式的任何元數據。 我們在這里添加:

%%R -i data -i data_tod -i genes -i cells -i soupx_groups -o out 

# specify row and column names of data
rownames(data) = genes
colnames(data) = cells
# ensure correct sparse format for table of counts and table of droplets
data <- as(data, "sparseMatrix")
data_tod <- as(data_tod, "sparseMatrix")

# Generate SoupChannel Object for SoupX 
sc = SoupChannel(data_tod, data, calcSoupProfile = FALSE)

# Add extra meta data to the SoupChannel object
soupProf = data.frame(row.names = rownames(data), est = rowSums(data)/sum(data), counts = rowSums(data))
sc = setSoupProfile(sc, soupProf)
# Set cluster information in SoupChannel
sc = setClusters(sc, soupx_groups)

# Estimate contamination fraction
sc  = autoEstCont(sc, doPlot=FALSE)
# Infer corrected table of counts and rount to integer
out = adjustCounts(sc, roundToInt = TRUE)

SoupX成功推斷出校正后的計數,我們現在可以將其存儲為附加層layer。在以下所有分析步驟中,我們希望使用SoupX校正計數矩陣,因此我們用soupX校正矩陣覆蓋.X

adata.layers["counts"] = adata.X
adata.layers["soupX_counts"] = out.T
adata.X = adata.layers["soupX_counts"]

接下來,我們另外過濾掉少于20個細胞中未檢測到的基因,因為這些基因沒有提供信息。

print(f"Total number of genes: {adata.n_vars}")

# Min 20 cells - filters out 0 count genes
sc.pp.filter_genes(adata, min_cells=20)
print(f"Number of genes after cell filter: {adata.n_vars}")

輸出結果:

Total number of genes: 36601
Number of genes after cell filter: 20171

4.5 雙聯體檢測

雙聯體被定義為在相同細胞條形碼下測序的兩個細胞,它們是在同一液滴中捕獲的。這就是為什么我們到目前為止使用術語“條形碼”而不是“細胞”。如果雙聯體由相同細胞類型(但來自不同個體)形成,則稱為同型,否則稱為異型。同型雙聯體不一定可以從計數矩陣中識別出來,并且通常被認為是無害的,因為它們可以通過細胞散列或SNP來識別。因此,它們的識別不是雙聯體檢測方法的主要目標。
由不同細胞類型或狀態形成的雙聯體稱為異型。它們的識別至關重要,因為它們很可能被錯誤分類,并可能導致下游分析步驟的錯誤。因此,雙聯體檢測和去除通常是初始預處理步驟。雙聯體可以通過其大量讀取和檢測到的特征來識別,也可以通過創建人工雙聯體并將其與數據集中存在的細胞進行比較的方法來識別。雙聯體檢測方法計算效率高,并且存在多個用于此任務的軟件包。
在本教程中,我們將展示scDblFinder R包。scDblFinder隨機選擇兩個液滴,并通過平均它們的基因表達譜來創建人工雙聯體。然后,雙聯體得分定義為主成分空間中每個液滴的k最近鄰圖中人工雙聯體的比例。

我們首先載入一些python和R包。

%%R
library(Seurat)
library(scater)
library(scDblFinder)
library(BiocParallel)
data_mat = adata.X.T

現在,我們可以使用data_mat作為SingleCellExperiment中scDblFinder的輸入來啟動雙聯體檢測。scBblFinder向sce的colData添加幾列。其中三個主要的組成:

  • sce$scDblFinder.score:最終的雙聯體得分(越高,細胞越有可能是雙聯體)
  • sce$scDblFinder.ratio:細胞鄰域中人工雙聯體的比例
  • sce$scDblFinder.class:分類(雙聯體或單聯體)

我們只會輸出參數并將其存儲在.obs的AnnData對象中。 其他參數可以類似地添加到AnnData對象。

%%R -i data_mat -o doublet_score -o doublet_class

set.seed(123)
sce = scDblFinder(
    SingleCellExperiment(
        list(counts=data_mat),
    ) 
)
doublet_score = sce$scDblFinder.score
doublet_class = sce$scDblFinder.class

scDblFinder輸出具有分類Singlet(1) 和Doublet (2) 的類。我們將其添加到.obs中的AnnData對象中。

adata.obs["scDblFinder_score"] = doublet_score
adata.obs["scDblFinder_class"] = doublet_class
adata.obs.scDblFinder_class.value_counts()

輸出結果:

singlet    11956
doublet     2858
Name: scDblFinder_class, dtype: int64

我們建議暫時將已識別的雙聯體留在數據集中,并在可視化過程中檢查雙聯體。
在下游聚類過程中,重新評估質量控制和所選參數可能會很有用,以過濾掉更多或更少的細胞。我們現在可以保存數據集并繼續之后的標準化過程。

adata.write("s4d8_quality_control.h5ad")
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370