이번 실습에서는 데이터의 시각화를 위한 차원축소 방법인 PCA, tSNE, UMAP등에 대해 배우고 배치 효과를 제거해서 여러 데이터셋을 통합하는 방법에 대해 살펴봅니다.
1 들어가며
이 글은 NBIS workshop 자료를 기반으로 수정 및 번역 작업을 진행한것입니다. 가장 먼저 필요한 라이브러리와 QC를 진행한 데이터 파일을 불러오도록 하겠습니다.
import warningsimport bbknnimport matplotlib.pyplot as pltimport numpy as npimport scanoramaimport scanpy as sc# 경고 무시하기warnings.filterwarnings("ignore")sc.settings.n_jobs =-1sc.settings.verbosity =0# 오류 (0), 경고 (1), 정보 (2), 힌트 (3)sc.settings.set_figure_params( dpi=60, frameon=False, figsize=(5, 5), facecolor="white", color_map="viridis_r")print(f"사용한 SCANPY 버전: {sc.__version__}")
사용한 SCANPY 버전: 1.10.1
/mnt/c4a1c56d-0589-48cd-9717-f6c33a86fa3b/scRNAseq/notebook_2024/scRNA_workshop/.pixi/envs/default/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
from .autonotebook import tqdm as notebook_tqdm
정규화되지 않은 원시 카운트 값이 확인이 되네요. 이 값을 다른 레이어로 저장하고 결측치(값이 없는 데이터)가 있는지 확인해봅니다.
# 원본 Count 값을 새로운 레이어로 저장adata.layers["counts"] = adata.X.copy()# X 및 var의 원본을 .raw.X 및 .raw.var로 저장.adata.raw = adata# 행이나 열방향으로 0으로만 존재하는 값이 있는지 확인print(np.any(adata.X.sum(axis=0) ==0))print(np.any(adata.X.sum(axis=1) ==0))
False
False
결측치도 없는 것 같습니다. 이제 데이터를 정규화하고 로그 변환합니다.
# normalize to depth 10,000sc.pp.normalize_per_cell(adata, counts_per_cell_after=1e4)# log transformsc.pp.log1p(adata)adata.layers["log1p"] = adata.X.copy()adata
scRNA-seq 데이터셋에는 특성(유전자 정보)이 아주 많습니다. 다만 대부분이 0의 값을 가지며 또한 일정한 발현 값을 가져 세포 유형을 구분할때 도움이 되지 않습니다. 따라서 세포 간에 발현량이 차이나는(가변적인) 특성만을 골라내 계산량을 줄일 수 있습니다.
# 가변 유전자 계산sc.pp.highly_variable_genes(adata, min_mean=0.0125, max_mean=3, min_disp=0.5)print("Highly variable genes: %d"%sum(adata.var.highly_variable))# 가변 유전자 플롯sc.pl.highly_variable_genes(adata)
Highly variable genes: 2611
3 스케일링(Z-점수)
유효한 특성(유전자)을 선택한 다음 차원 축소를 위해 PCA를 진행해야 합니다. 그런데 유전자마다 발현 수준이 모두 다르기 때문에 PCA를 진행하기 전에 각 유전자에 가중치를 부여해서 값을 스케일링해줘야 합니다. 일반적으로는 Z-점수 스케일링 방법을 많이 사용합니다. Z-점수 스케일링은 표준 편차가 1이고 평균이 0으로 하는 정규 분포가 되도록 값을 조정합니다.
# 데이터를 스케일링 합니다. 표준 편차 10이 넘는 값은 제거합니다.sc.pp.scale(adata, max_value=10)np.std(adata.X)
0.8287251754679223
4 PCA(주성분 분석)
데이터의 차원을 축소하기 위해 PCA를 수행합니다. PCA는 사용되는 데이터에 따라 많은 유용한 응용과 해석이 가능합니다. scRNA seq의 경우 데이터의 유전자 발현 패턴에 따라 세포 유형을 구분하고자 합니다. scanpy에서 PCA를 실행하려면 pca() 함수를 사용합니다.
sc.tl.pca(adata, svd_solver="arpack")# 그런 다음 첫 번째 주성분에 대한 플롯을 그려봅니다.sc.pl.pca(adata, color="sample", components=["1,2", "3,4", "5,6", "7,8"], ncols=2)
각 PC에 가장 많이 기여하는 유전자를 식별하기 위해 다음과 같이 각 PC의 정보를 시각화 할 수 있습니다.
위 그림을 보면 상위 30개의 PC는 많은 정보를 보유하고 있는 반면 그 아래 PC는 점점 더 적은 정보를 포함하고 있음을 알 수 있습니다. 따라서 상위 30개 정도의 PC를 사용해 이후 tSNE, UMAP을 그려보겠습니다.
Note
희귀한 세포의 경우(예: 이 데이터셋에서는 혈소판 및 DC) 하위 PC에만 그런 정보가 포함되어 있을 수 있음으로 일반적으로 많은 PC를 사용해 진행하는 것이 권장됩니다.
5 tSNE
tSNE(t-distributed stochastic neighbor embedding)은 높은 차원의 복잡한 데이터를 2차원에 차원 축소하는 방법입니다. 시각화에 주로 사용하며 데이터 구조를 이해하는 데 도움을 줍니다. 샘플별로 색을 다르게 한 tSNE을 그려봅니다. 그렇게 하면 데이터 집합에 존재하는 배치의 효과를 명확하게 확인할 수 있습니다.
UMAP은 tSNE와 비교해서 속도가 빠르다는 장점과 데이터의 전체 구조를 더 잘 보존한다는 장점을 가지고 있습니다. 그래서 점점 UMAP이 많이 사용되는 추세입니다. scanpy에서 UMAP을 그리는 코드는 아래와 같습니다. 먼저 이웃(neighbors) 그래프를 계산하고 tl.umap 함수를 사용합니다.
# 이웃 그래프 계산sc.pp.neighbors(adata, n_pcs=30, n_neighbors=20)sc.tl.umap(adata)sc.pl.umap(adata, color="sample")
위 그림에서 tSNE만큼 뚜렷하지는 않지만 샘플에 의한 배치 효과가 여전히 뚜렷하게 보입니다. 특히 covid_17 샘플이 말이죠.
UMAP와 t-SNE는 둘 다 고차원 데이터를 저차원 공간에 시각화하기 위해 사용되는 차원 축소 기법입니다. 둘은 많은 유사점을 가지지만 다음과 같은 차이점도 있습니다.
알고리즘:
t-SNE (t-Distributed Stochastic Neighbor Embedding): 고차원 공간에서 데이터 포인트 간의 유사성을 저차원 공간에서의 유사성으로 변환하는 데 주로 확률적 접근을 사용합니다. 저차원 공간에서 유사성을 표현하기 위해 t-분포를 사용하여 클러스터 간의 명확한 경계를 생성합니다.
UMAP (Uniform Manifold Approximation and Projection): 기하학적 및 위상수학적 이론에 기반하여 데이터의 기하학적 구조를 보존합니다. UMAP는 k-최근접 이웃을 사용하여 고차원 공간의 근접성을 유지하고 이를 저차원 공간으로 매핑합니다.
속도와 확장성:
t-SNE는 데이터 포인트가 많아질수록 계산량이 급격히 증가하는 경향이 있습니다. 따라서 대규모 데이터셋에 적용하기가 어렵습니다.
UMAP는 일반적으로 t-SNE보다 빠르고, 대규모 데이터셋에도 더 잘 확장됩니다.
보존되는 구조:
t-SNE는 로컬 구조(즉, 개별 클러스터 내부의 데이터 관계)에 집중하며, 전체 구조(클러스터 간의 관계를 잘 보존하지 못할 수 있습니다.
UMAP는 로컬 구조뿐만 아니라 전체 구조도 어느 정도 보존하려고 노력합니다.
매개변수:
t-SNE의 주요 매개변수는 perplexity로, 이는 클러스터의 밀도에 영향을 미칩니다.
UMAP는 n_neighbors 및 min_dist와 같은 여러 매개변수를 가지며 로컬 및 전체 구조 보존에 영향을 미칩니다.
먼저 BBKNN(Batch Balanced KNN)을 실행해 보겠습니다. BBKNN은 빠르고 직관적인 배치 효과 제거 도구로 scanpy 워크플로우에서 바로 사용할 수 있습니다. 또한 scanpy.pp.neighbors() 함수 대신에 사용할 수 있습니다. 더 자세한 내용은 BBKNN 문서를 참조하세요.
Scanorama는 scRNA-seq 데이터 세트의 일괄 보정 및 통합을 사용할 수 있도록 설계되었습니다. Scanorama를 통한 보정의 결과는 다른 도구의 입력으로 사용하여 scRNA-seq 클러스터링, 시각화 및 분석에 사용할 수 있습니다. 이제 Scanorama도 사용해 보겠습니다. 먼저, 각 데이터 세트에서 개별 AnnData 객체를 만들어야 합니다.
# 배치별로 개체를 분할합니다.batches = adata.obs["sample"].cat.categories.tolist()alldata = {}for batch in batches: alldata[batch] = adata[adata.obs["sample"] == batch,]# 개별 데이터 집합을 처음에 정의한 가변 유전자로 하위 집합합니다.alldata2 = {}for ds in alldata.keys():print(ds) alldata2[ds] = alldata[ds][:, var_genes]# AnnData 객체 목록으로 변환adatas =list(alldata2.values())# scanorama.integrate 실행scanorama.integrate_scanpy(adatas, dimred=50, verbose=False)# scanorama는 adatas의 각 데이터 집합에서 수정된 행렬을 adata.obsm에 추가합니다.adatas[0].obsm["X_scanorama"].shape
covid_1
covid_15
covid_16
covid_17
ctrl_5
ctrl_13
ctrl_14
ctrl_19
Found 4268 genes among all datasets
(874, 50)
# 모든 통합 행렬을 가져옵니다.scanorama_int = [ad.obsm["X_scanorama"] for ad in adatas]# 하나의 행렬로 만듭니다.all_s = np.concatenate(scanorama_int)print(all_s.shape)# AnnData 객체에 추가하려면 먼저 새 객체를 만듭니다.adata_sc = adata.copy()adata_sc.obsm["Scanorama"] = all_s
(7227, 50)
# tsne 과 umap을 그립니다.sc.pp.neighbors(adata_sc, n_pcs=30, use_rep="Scanorama")sc.tl.umap(adata_sc)sc.tl.tsne(adata_sc, n_pcs=30, use_rep="Scanorama")
배치 효과 제거를 위해 사용할 수 있는 다양한 방법이 있으며, 각 방법에는 장단점이 있습니다. 가장 적합한 방법을 선택하는 데에는 데이터의 특성과 분석 목표에 따라 달라질 수 있습니다. 그러니 다양한 방법을 시도하여 데이터 특성에 가장 잘 맞는 방법을 찾는 것이 중요합니다.
Note
배치 효과가 잘 제거되었는지 어떻게 평가할 수 있을까요? 크게 두가지 방법이 있습니다.
시각적 평가: UMAP 플롯을 통해 배치가 잘 섞여 있는지 시각적으로 확인합니다. 배치 간의 구분이 사라지면 배치 효과가 잘 제거된 것입니다.
실루엣 스코어: 높은 실루엣 스코어는 잘 클러스터링된 데이터를 의미합니다. 배치 효과 제거 전후의 실루엣 스코어를 비교하여 배치 효과 제거의 효과를 평가합니다.
시각적 평가를 통해 어떤 도구가 배치 효과 보정을 잘 했는지 살펴보면, 아무래도 Scanorma로 한 것이 좋아보입니다. 이후의 분석은 adata_sc 객체를 가지고 진행하겠습니다.
이번 글에서는 scRNAseq 데이터 분석에서 중요한 두 가지 주제인 차원 축소와 배치 효과 제거에 대해 다루었습니다. 차원 축소를 통해 데이터를 효과적으로 시각화하고 해석할 수 있었으며 배치 효과 제거 도구를 사용하는 방법을 배웠습니다. 다음 글에서는 더 흥미로운 주제를 가지고 다시 찾아뵙겠습니다.