덩어리는 보이는데, 그게 무슨 세포인가요
S29에서 UMAP 지도 위에 세포 덩어리들이 나타났습니다. 하지만 지도는 "여기 뭔가 모여 있다"만 보여줄 뿐, 그 덩어리가 T세포인지 B세포인지는 말해주지 않습니다. 두 가지가 필요합니다. 먼저 세포를 객관적 알고리즘으로 그룹화하고(눈대중이 아니라), 각 그룹의 마커 유전자로 정체를 밝히는 것.
핵심은 S29에서 만든 kNN 그래프입니다. 세포를 노드, 이웃 관계를 엣지로 보면, "세포 유형"은 곧 서로 촘촘히 연결된 노드 뭉치 = 커뮤니티입니다. 클러스터링이 그래프 커뮤니티 검출 문제로 바뀝니다.
좋은 분할이란 — modularity
커뮤니티를 어떻게 "좋다"고 판단할까요? 지표는 모듈성(modularity) 입니다.
는 실제 엣지, 는 무작위 그래프에서 기대되는 엣지. 즉 는 **"같은 커뮤니티 안의 연결이 우연보다 얼마나 많은가"**를 잽니다. 가 클수록 커뮤니티 안은 촘촘하고 커뮤니티 사이는 성깁니다. 클러스터링은 이 를 최대화하는 분할을 찾는 문제가 됩니다.
이 최적화는 NP-난해라 근사 알고리즘을 씁니다. 오래 쓰인 것이 Louvain입니다. 각 노드를 이웃 커뮤니티로 옮겨보며 가 오르면 확정하는 그리디 방식입니다.
왜 Louvain이 아니라 Leiden인가
Louvain에는 알려진 결함이 있습니다. 끊긴 커뮤니티(disconnected community) 를 만들 수 있다는 것입니다. 최적화 도중 한 커뮤니티가 실제로는 연결되지 않은 두 조각으로 쪼개졌는데도 하나로 라벨되는 경우가 생깁니다. 세포 분석에서는 "한 클러스터인데 실은 서로 안 이어진 두 세포군"이라는 말이 안 되는 결과입니다.
Leiden은 이 문제를 수학적으로 해결합니다. refinement 단계를 추가해 모든 커뮤니티가 내부적으로 잘 연결됨을 보장하고, 더 빠르고 더 나은 에 수렴합니다. 오늘날 Scanpy·Seurat의 기본 클러스터링은 Leiden입니다.
핵심 파라미터는 resolution입니다. 높이면 커뮤니티를 잘게(클러스터 많이), 낮추면 크게(클러스터 적게) 나눕니다. "정답 개수"는 없습니다. 생물학적 질문에 맞춰 조정하는 탐색 파라미터입니다.
손으로 modularity 감 잡기
노드 6개가 삼각형 두 개 3, 6로 촘촘히 뭉치고 두 삼각형은 엣지 하나로만 연결됐다고 합시다. 이 자연스러운 두 그룹으로 나누면 커뮤니티 내부 엣지(각 삼각형 3개씩)가 기대치보다 훨씬 많아 가 큽니다. 반대로 4,5,6처럼 엮으면 내부 연결이 거의 없어 가 0 근처로 떨어집니다. 알고리즘은 전자를 고릅니다 — 연결이 촘촘한 대로 자르는 것이 modularity의 직관입니다.
마커 유전자 — 클러스터에 이름 붙이기
클러스터를 얻었으면, 각 클러스터가 다른 클러스터 대비 특이하게 높게 발현하는 유전자를 찾습니다. 이것이 마커입니다. 검정은 클러스터 vs 나머지 전체의 차등 발현으로, scRNA-seq에서는 보통 Wilcoxon 순위합 검정을 씁니다(정규분포 가정이 약해도 견고).
마커를 알려진 세포 유형 지식과 대조하면 이름이 붙습니다. PBMC라면:
| 마커 | 세포 유형 |
|---|---|
| CD3D, IL7R | T 세포 |
| MS4A1, CD79A | B 세포 |
| NKG7, GNLY | NK 세포 |
| CST3, LYZ | 단핵구/수지상세포 |
| PPBP | 혈소판 |
이 대조가 scRNA-seq 분석의 하이라이트입니다. 익명의 클러스터 0~8이 "CD4 T세포", "B세포"로 살아나는 순간입니다.
Scanpy 실습 (PBMC 3k)
S29의 kNN 그래프에서 바로 이어집니다.
import scanpy as sc# adata = S29 (neighbors, umap 계산 완료)
# Leiden 클러스터링sc.tl.leiden(adata, resolution=1.0, flavor="igraph", n_iterations=2)sc.pl.umap(adata, color="leiden", legend_loc="on data")
# 마커 유전자 (클러스터 vs 나머지, Wilcoxon)sc.tl.rank_genes_groups(adata, "leiden", method="wilcoxon")sc.pl.rank_genes_groups(adata, n_genes=10, sharey=False)
# 알려진 마커로 정체 확인 (dotplot)markers = ["CD3D", "IL7R", "MS4A1", "CD79A", "NKG7", "GNLY", "CST3", "LYZ", "PPBP"]sc.pl.dotplot(adata, markers, groupby="leiden")dotplot에서 점 크기는 "그 클러스터에서 발현하는 세포 비율", 색은 "평균 발현 세기"입니다. 어떤 클러스터에서 CD3D 점이 크고 진하면 그 클러스터는 T세포입니다. 이렇게 하나씩 대응해 라벨을 답니다.
new_labels = {"0": "CD4 T", "1": "B", "2": "CD14 Mono", "3": "NK", "4": "CD8 T"}adata.obs["cell_type"] = adata.obs["leiden"].map(new_labels).astype("category")sc.pl.umap(adata, color="cell_type", legend_loc="on data")resolution을 0.5, 1.0, 2.0으로 바꿔 클러스터 수가 어떻게 변하는지 꼭 확인합시다. 너무 잘게 나누면 하나의 세포 유형이 여러 조각으로, 너무 뭉치면 다른 유형이 섞입니다. 마커의 생물학적 일관성으로 적정값을 판단하는 것이 실무 감각입니다.
CS 매핑
- 커뮤니티 검출: Leiden은 그래프 커뮤니티 검출 알고리즘입니다. 소셜 네트워크 분석·추천 시스템과 같은 도구입니다.
- 그래프 분할 / modularity 최적화: 최대화는 NP-난해 최적화의 근사 문제로, DryBench의 그래프 알고리즘과 직결됩니다.
- 그리디 최적화: Louvain/Leiden의 노드 이동은 지역 개선을 반복하는 그리디 전략입니다. Leiden의 refinement는 지역 최적 탈출 장치입니다.
- 비모수 검정(Wilcoxon): 마커 검정은 분포 가정이 약한 순위 기반 검정으로, 통계의 비모수 방법입니다.
자주 만나는 결함
- resolution 하나로 결론 — 정답 개수는 없습니다. 여러 값을 시도하고 마커 일관성으로 판단합시다.
- 여전히 Louvain 사용 — 끊긴 커뮤니티 위험이 있습니다. Leiden이 표준입니다.
- 마커 검정을 스케일링/PCA 값으로 — 마커는 로그 정규화값(
adata.raw)으로 검정합니다. 스케일된 값은 부호·크기가 왜곡됩니다. - 배치가 안 섞인 채 클러스터링 — 서로 다른 실험 배치가 별도 클러스터로 갈라져 "가짜 세포 유형"이 생깁니다. 다음 편 S31의 배치 보정이 선행돼야 하는 경우가 많습니다.
더 깊게 파고 싶다면
본문은 BPD가 직접 재구성한 서술입니다. 심화는 아래 정통 자료를 활용합시다.
- 참고 무료 웹북: Single-cell best practices (sc-best-practices.org) — 클러스터링·주석 챕터가 resolution 선택과 주석 전략을 상세히 다룹니다.
- 원 논문(Leiden): Traag, Waltman, van Eck (2019), From Louvain to Leiden: guaranteeing well-connected communities, Scientific Reports 9:5233. 끊긴 커뮤니티 문제의 해결이 핵심입니다.
- 원 논문(Louvain): Blondel et al. (2008), Fast unfolding of communities in large networks, J. Stat. Mech. P10008.
- EMBL-EBI Training — Clustering and cell type annotation (자막 완비). 마커 기반 주석의 실무 절차를 줍니다.
이제 세포 유형이 붙은 지도가 완성됐습니다. 그런데 여러 환자·여러 실험의 데이터를 합치면 새 문제가 생깁니다 — 세포 유형이 아니라 실험 배치로 갈라지는 것입니다. 다음 편 S31에서 이 배치 효과를 제거하는 Harmony·scVI로 넘어가, 단일세포 시리즈를 마무리합니다.