scanpy로 단일세포 RNA-seq 분석하기 — QC부터 UMAP·클러스터까지 (Python for Bio)

TL;DR — 단일세포 RNA-seq (scRNA-seq) 데이터를 파이썬으로 분석하는 사실상 표준이 scanpy (라이브러리)입니다. 공개 데이터 pbmc3k (말초혈액 단핵세포 2,700개)를 받아서 품질관리(QC) → 정규화(normalization) → 고변이 유전자(HVG) → 주성분분석(PCA) → 이웃그래프(neighbors) → UMAP → Leiden 군집화(clustering) → 마커 유전자(marker gene)까지, 한 번에 끝까지 가 봅니다.

핵심 자료구조는 AnnData입니다. 발현 행렬을 .X에, 세포 정보를 .obs에, 유전자 정보를 .var에 담아 한 객체로 들고 다닙니다. 분석을 진행하면 PCA·UMAP 좌표(.obsm)와 군집 라벨(.obs)이 이 객체에 차곡차곡 쌓입니다.

R의 Seurat로 단일세포 RNA-seq 분석같은 pbmc3k 데이터, 같은 흐름을 파이썬으로 옮긴 글입니다. 함수가 1:1로 대응하므로, Seurat을 써 봤다면 그대로 번역해 읽으면 됩니다.

🔗 관련 글  ·  R로 같은 분석을 하려면 Seurat로 단일세포 RNA-seq 분석  ·  scRNA-seq가 무엇을 보는지부터 알고 싶다면 단일세포 RNA 시퀀싱(scRNA-seq)이란?

이 글에서 만들 것

scRNA-seq 개념 글에서 봤듯이, 단일세포 RNA-seq는 조직 전체의 평균이 아니라 세포 하나하나의 발현을 따로 읽습니다. 그렇게 얻은 데이터는 세포 수천~수십만 개 × 유전자 2만~3만 개짜리 거대한 행렬입니다. 이걸 가지고 "여기 어떤 세포 종류가 몇 개 섞여 있나"를 알아내는 게 분석의 첫 목표입니다.

파이썬에서 이 작업의 표준 도구가 scanpy (Wolf et al. 2018)입니다. R 진영의 Seurat과 짝을 이루는 도구로, 대용량 데이터를 빠르고 메모리 효율적으로 다루도록 설계됐습니다. 이 글에서는 10x Genomics가 공개한 pbmc3k (건강한 사람의 말초혈액 단핵세포 2,700개)를 scanpy에 내장된 데이터로 바로 불러와, 품질관리부터 군집화·셀타입 해석까지 전 과정을 한 번 통과합니다. 모두 공개 데이터라 그대로 따라 하면 똑같이 재현됩니다.

전체 흐름은 아래 한 덩어리로 요약됩니다. 각 단계가 AnnData 객체에 결과를 더해 나가는 구조입니다.

# 단일세포 분석 핵심 흐름 한눈에 (scanpy)
import scanpy as sc
adata = sc.datasets.pbmc3k()                 # ① 데이터 로드 → AnnData
sc.pp.calculate_qc_metrics(adata, ...)       # ② 품질관리(QC) 지표 계산·필터
sc.pp.normalize_total(adata, target_sum=1e4) # ③ 정규화
sc.pp.log1p(adata)                           #    + 로그 변환
sc.pp.highly_variable_genes(adata, ...)      # ④ 고변이 유전자(HVG) 선별
sc.pp.scale(adata, max_value=10)             # ⑤ 스케일링
sc.tl.pca(adata)                             # ⑥ 주성분분석(PCA)
sc.pp.neighbors(adata, n_neighbors=10, n_pcs=40)  # ⑦ 이웃그래프
sc.tl.umap(adata)                            # ⑧ UMAP (차원축소·시각화)
sc.tl.leiden(adata, flavor="igraph")         # ⑨ Leiden 군집화
sc.tl.rank_genes_groups(adata, "leiden")     # ⑩ 마커 유전자

1. 사전 준비 — 설치와 데이터

scanpy는 PyPI (파이썬 패키지 저장소)에서 바로 설치됩니다. 자료구조를 제공하는 anndata는 의존성으로 함께 깔리므로 따로 설치할 필요가 없습니다. 다만 Leiden 군집화에 필요한 그래프 라이브러리는 별도입니다. 한 번에 다 받으려면 [leiden] 확장(extra)을 쓰는 게 깔끔합니다.

# 기본 설치 (anndata 자동 포함)
pip install scanpy

# Leiden 군집화까지 한 번에 (igraph + leidenalg 함께 설치) — 권장
pip install "scanpy[leiden]"
💡 버전 메모 (2025~2026) — 글 작성 시점의 최신은 scanpy 1.12 계열이고 파이썬 3.12 이상, anndata 0.10 이상을 요구합니다. 재현성이 중요하면 pip install "scanpy==1.12.*"처럼 버전을 고정해 두는 편이 안전합니다. Leiden 군집화의 기본 동작이 버전에 따라 바뀌었기 때문입니다(아래 7단계에서 설명).

설치가 끝나면 데이터를 불러옵니다. scanpy는 학습용 공개 데이터를 sc.datasets 아래에 내장해 두었습니다. pbmc3k()는 가공 전 원본 카운트(raw count)를 돌려줍니다.

import scanpy as sc

sc.settings.verbosity = 1            # 로그 간단히
adata = sc.datasets.pbmc3k()         # 말초혈액 단핵세포 2,700개 (10x)
adata
AnnData object with n_obs × n_vars = 2700 × 32738
    var: 'gene_ids'

여기서 돌려받은 adataAnnData 객체입니다. n_obs × n_vars = 2700 × 32738세포 2,700개 × 유전자 32,738개라는 뜻입니다. scanpy에서는 관습적으로 행(observation)이 세포, 열(variable)이 유전자입니다. R의 Seurat과 행·열이 반대라 처음엔 헷갈리는 지점이니 기억해 두세요.

AnnData 구조 — .X · .obs · .var

AnnData는 발현 행렬 하나만 들고 있는 게 아니라, 분석에 필요한 정보를 한 객체에 묶어 둡니다. 핵심은 세 칸입니다.

담는 것 모양 pbmc3k 예시
.X발현 행렬 (보통 희소행렬)세포 × 유전자2700 × 32738 카운트
.obs세포별 정보 (표)행 = 세포QC 지표·군집 라벨
.var유전자별 정보 (표)행 = 유전자유전자 ID·HVG 여부
.obsm세포별 다차원 배열행 = 세포X_pca·X_umap 좌표
.uns비정형 정보 (딕셔너리)군집 색·이웃그래프 설정
.layers.X와 같은 모양의 다른 행렬세포 × 유전자원본 카운트 보관

분석을 진행하면 이 칸들이 채워집니다. QC 지표는 .obs에, HVG 표시는 .var에, PCA·UMAP 좌표는 .obsm에 들어가는 식이죠. 그래서 한 객체만 들고 다니면 분석의 모든 상태가 따라옵니다 — 이게 AnnData를 쓰는 이유입니다.

adata.X[:5, :5].toarray()    # 발현 행렬 일부 (희소행렬 → 밀집 변환)
adata.obs.head()             # 세포별 표 (지금은 비어 있음, 곧 채워짐)
adata.var.head()             # 유전자별 표 (gene_ids 열만 있음)

유전자 이름이 중복되는 경우가 있어, 본격적인 분석 전에 한 번 정리해 두면 좋습니다.

adata.var_names_make_unique()   # 중복 유전자명에 접미사 붙여 고유화

2. 품질관리(QC) — 죽은 세포·빈 방울 걸러내기

원본 데이터에는 분석에 방해가 되는 세포가 섞여 있습니다. 이미 죽어 가는 세포(미토콘드리아 RNA 비율이 높음), 두 세포가 한 방울에 잡힌 더블릿(doublet, 유전자 수가 비정상적으로 많음), 세포 없이 주변 RNA만 잡힌 빈 방울(유전자 수가 너무 적음) 같은 것들이죠. 이걸 걸러내는 단계가 품질관리(QC)입니다.

먼저 미토콘드리아 유전자에 표시를 답니다. 사람 유전자에서 미토콘드리아 유전자는 이름이 MT-로 시작합니다.

# 미토콘드리아 유전자 표시 (이름이 MT- 로 시작)
adata.var["mt"] = adata.var_names.str.startswith("MT-")

# QC 지표 계산 → .obs 와 .var 에 자동으로 열이 추가됨
sc.pp.calculate_qc_metrics(
    adata, qc_vars=["mt"], percent_top=None, log1p=False, inplace=True
)
adata.obs[["n_genes_by_counts", "total_counts", "pct_counts_mt"]].head()
                  n_genes_by_counts  total_counts  pct_counts_mt
AAACATACAACCAC-1                779        2419.0       3.017776
AAACATTGAGCTAC-1               1352        4903.0       3.793596
AAACATTGATCAGC-1               1129        3147.0       0.889736
AAACCGTGCTTCCG-1                960        2639.0       1.743085
AAACCGTGTATGCG-1                521         980.0       1.224490

calculate_qc_metrics가 세포별 핵심 지표 세 가지를 .obs에 만들어 줍니다. n_genes_by_counts는 세포가 발현한 유전자 수, total_counts는 총 카운트(=라이브러리 크기), pct_counts_mt는 미토콘드리아 유전자가 차지하는 비율(%)입니다. 분포를 그림으로 먼저 보고 문턱값을 정합니다.

# 세 지표의 분포를 바이올린 플롯으로
sc.pl.violin(
    adata, ["n_genes_by_counts", "total_counts", "pct_counts_mt"],
    jitter=0.4, multi_panel=True,
)
# 유전자 수 vs 미토 비율 산점도 (관계 확인)
sc.pl.scatter(adata, x="total_counts", y="pct_counts_mt")
sc.pl.scatter(adata, x="total_counts", y="n_genes_by_counts")

분포를 보고 나면 필터링입니다. pbmc3k 표준 튜토리얼이 쓰는 문턱값은 이렇습니다 — 유전자 200개 미만 세포 제거, 3개 미만 세포에서만 잡힌 유전자 제거, 유전자 수 2,500개 초과(더블릿 의심) 제거, 미토 비율 5% 초과(손상 세포) 제거.

# 세포·유전자 필터
sc.pp.filter_cells(adata, min_genes=200)    # 유전자 200개 미만 세포 제거
sc.pp.filter_genes(adata, min_cells=3)      # 3개 미만 세포에서만 잡힌 유전자 제거

# 더블릿·손상 세포 제거 (불리언 인덱싱)
adata = adata[adata.obs.n_genes_by_counts < 2500, :].copy()
adata = adata[adata.obs.pct_counts_mt < 5, :].copy()
adata
AnnData object with n_obs × n_vars = 2638 × 13714
    obs: 'n_genes_by_counts', 'total_counts', ..., 'pct_counts_mt'
    var: 'gene_ids', 'mt', 'n_cells_by_counts', ...

세포가 2,700개에서 2,638개로, 유전자가 32,738개에서 13,714개로 줄었습니다. 문턱값은 데이터마다 다릅니다 — 조직·실험 플랫폼에 따라 분포가 달라지므로, 위 그림을 보고 그때그때 정해야 합니다. 숫자를 기계적으로 베끼지 말고 분포에 맞추세요.

⚠️ 불리언 인덱싱 뒤엔 .copy()adata[조건, :]처럼 일부만 잘라내면 scanpy는 원본을 가리키는 '뷰(view)'를 돌려줍니다. 여기에 바로 값을 쓰면 ImplicitModificationWarning이 뜹니다. 위처럼 .copy()를 붙여 독립된 객체로 만들면 안전합니다.

3. 정규화(normalization)와 로그 변환

세포마다 잡힌 총 카운트(시퀀싱 깊이)가 다릅니다. 어떤 세포는 5,000개, 어떤 세포는 1,000개가 잡혔다면 그 차이는 생물학이 아니라 기술적 변동입니다. 이걸 맞춰 주는 게 정규화입니다. scanpy의 표준은 세포마다 총합을 같은 값(보통 10,000)으로 맞춘 뒤(normalize_total), 로그를 취하는(log1p) 두 단계입니다.

# 나중을 위해 원본(로그 정규화 직전)을 .raw 에 보관 — 마커 시각화에 씀
sc.pp.normalize_total(adata, target_sum=1e4)   # 세포별 총합을 1만으로 맞춤 (CP10k)
sc.pp.log1p(adata)                             # log(1+x) 변환
adata.raw = adata                              # 이 상태(전체 유전자)를 .raw 에 저장

normalize_total(target_sum=1e4)는 흔히 CP10k (counts per 10,000)라 부릅니다. 이어지는 log1plog(1+x)를 취해 발현값의 분포를 다루기 쉽게 펴 줍니다. 1을 더하는 건 발현이 0인 유전자의 로그가 음의 무한대로 가는 걸 막기 위해서입니다. 참고로 R의 Seurat에서 NormalizeData() 기본값도 이 CP10k + 로그와 같은 방식입니다.

adata.raw = adata로 이 시점의 상태를 따로 저장해 둔 점에 주목하세요. 다음 단계에서 유전자를 HVG만 남기고 잘라낼 텐데, 마커 유전자를 그림으로 볼 때는 잘려 나간 유전자까지 포함한 전체 발현이 필요합니다. .raw에 넣어 두면 그때 꺼내 쓸 수 있습니다.

⚠️ log1p는 한 번만 — 이미 로그를 취한 데이터에 log1p를 또 실행하면 값이 망가집니다. scanpy는 이를 막으려고 adata.uns["log1p"]에 흔적을 남기고 "이미 로그 변환된 것 같다"고 경고합니다. 셀을 두 번 실행하지 않도록 주의하세요.

4. 고변이 유전자(HVG) 선별

3만 개 유전자를 다 쓰면 대부분은 세포 간 차이가 거의 없는 '배경'이라 군집을 흐립니다. 세포 종류를 가르는 정보는 세포마다 발현이 크게 출렁이는 유전자에 들어 있습니다. 이걸 골라내는 게 고변이 유전자(highly variable genes, HVG) 선별입니다.

# 고변이 유전자 선별 (기본 flavor="seurat", 로그 정규화 데이터 입력)
sc.pp.highly_variable_genes(
    adata, min_mean=0.0125, max_mean=3, min_disp=0.5
)
sc.pl.highly_variable_genes(adata)    # 평균발현 vs 분산 산점도 (HVG 강조)

# HVG 만 남기고 잘라내기
adata = adata[:, adata.var.highly_variable].copy()
adata
AnnData object with n_obs × n_vars = 2638 × 1838
    ...
    var: ..., 'highly_variable', 'means', 'dispersions', ...

유전자가 13,714개에서 1,838개로 줄었습니다. 이 1,838개가 세포 종류를 구분하는 데 정보가 많은 유전자들입니다. min_mean·max_mean·min_disp는 "평균 발현이 이 범위이고 분산이 이 값 이상인 유전자"를 HVG로 본다는 기준입니다. 위 숫자는 pbmc3k 표준 튜토리얼 값입니다.

📌 flavor에 따라 입력이 다르다 — 기본값 flavor="seurat"로그 정규화된 데이터를 받습니다(그래서 log1p 다음에 실행). 반면 flavor="seurat_v3"원본 카운트를 받고 n_top_genes를 반드시 지정해야 하며 scikit-misc 설치가 필요합니다. 입력 데이터를 잘못 넣어도 에러 없이 엉뚱한 유전자가 뽑히니, flavor와 데이터 상태를 꼭 맞추세요.
그림 1. QC·필터링 요약 — 세포는 품질관리로, 유전자는 고변이 선별(HVG)로 추려 분석 대상을 좁힌다. 문턱값은 분포를 보고 데이터마다 정한다.

5. 스케일링과 PCA — 차원 줄이기

군집화 전에 두 가지를 더 합니다. 먼저 각 유전자의 값을 평균 0, 분산 1로 맞추는 스케일링(scaling)입니다. 발현이 큰 유전자가 거리 계산을 지배하지 않도록 균형을 맞추는 작업이죠. 그다음 주성분분석(PCA)으로 1,838차원을 수십 차원으로 압축합니다.

# (선택) 총 카운트·미토 비율의 영향을 회귀로 제거 — 느리므로 생략 가능
# sc.pp.regress_out(adata, ["total_counts", "pct_counts_mt"])

sc.pp.scale(adata, max_value=10)      # 유전자별 z-score, 최대 10으로 클립
sc.tl.pca(adata, svd_solver="arpack") # 주성분분석

sc.pl.pca_variance_ratio(adata, n_pcs=50, log=True)  # 주성분별 설명력 (스크리 플롯)

scalemax_value=10은 z-score를 ±10에서 잘라(clip) 극단값이 분석을 흔드는 걸 막습니다. pca는 발현 패턴이 비슷한 유전자들을 묶어 '주성분(principal component)'이라는 새 축으로 데이터를 다시 표현합니다. 스크리 플롯(scree plot)에서 설명력이 꺾이는 지점을 보고, 다음 단계에서 쓸 주성분 개수(n_pcs)를 정합니다. pbmc3k에서는 보통 40개 안팎이면 충분합니다.

참고로 scanpy에는 sc.tl.pcasc.pp.pca가 둘 다 있고 결과는 같습니다. 표준 튜토리얼은 sc.tl.pca를 쓰는데, 최신 버전에서 더 적극적으로 관리되는 쪽은 sc.pp.pca입니다. 입문 단계에서는 어느 쪽을 써도 무방합니다.

6. 이웃그래프(neighbors)와 UMAP

이제 세포들을 PCA 공간에서의 가까움에 따라 그래프로 잇습니다. 각 세포를 가장 가까운 이웃 세포들과 연결한 k-최근접이웃(k-nearest neighbor) 그래프이고, 이 그래프가 군집화와 UMAP 양쪽의 토대가 됩니다.

# 이웃그래프: 주성분 40개를 써서 세포당 이웃 10개 연결
sc.pp.neighbors(adata, n_neighbors=10, n_pcs=40)

# UMAP: 그래프를 2차원에 펼쳐 시각화
sc.tl.umap(adata)
sc.pl.umap(adata)     # 아직 색칠 정보 없음 — 점만 펼쳐진 지형도

neighbors가 그래프를 만들면, UMAP (uniform manifold approximation and projection)이 그 그래프를 2차원 평면에 펼칩니다. 가까운 세포는 가깝게, 먼 세포는 멀게 배치해 세포들의 '지형도'를 그리는 차원축소(dimensionality reduction) 기법입니다. 이 시점의 UMAP은 색이 없어 점 구름만 보입니다. 색은 다음 단계에서 군집을 입혀 채웁니다. 참고로 UMAP은 시각화용이고, 거리·면적의 절대값에 큰 의미를 두면 안 됩니다 — 군집의 모양보다 '어떤 세포끼리 뭉치는가'를 보는 도구입니다.

그림 2. Leiden 군집을 색으로 입힌 UMAP — 같은 종류의 세포끼리 뭉쳐 섬을 이루고, 셀타입 이름을 붙이면 PBMC 구성이 한눈에 보인다.

7. Leiden 군집화(clustering)

군집화는 이웃그래프를 촘촘히 연결된 덩어리로 나누는 작업입니다. scanpy의 표준은 Leiden 알고리즘(Traag et al. 2019)입니다. 이전에 쓰이던 Louvain을 개량해, 잘게 끊긴 군집이 생기지 않도록 보장합니다.

# Leiden 군집화 — 최신 권장 호출 방식
sc.tl.leiden(
    adata,
    resolution=1.0,
    flavor="igraph",     # 빠른 igraph 백엔드 (scanpy 1.10+)
    n_iterations=2,
    directed=False,
)
sc.pl.umap(adata, color="leiden")    # 군집별 색으로 UMAP 색칠
adata.obs["leiden"].value_counts()   # 군집별 세포 수
leiden
0    647
1    623
2    441
3    323
...
Name: count, dtype: int64

UMAP에 군집별 색이 입혀지면서 세포 종류가 시각적으로 갈라집니다. resolution이 군집의 해상도를 정합니다 — 키우면 잘게 쪼개지고, 줄이면 뭉칩니다. pbmc3k에서는 1.0 안팎이면 PBMC의 주요 세포 종류 8개 정도로 잘 나뉩니다.

📌 flavor="igraph"는 왜 명시하나 — scanpy 1.10부터 flavor 없이 leiden을 부르면 FutureWarning이 뜹니다. "앞으로 기본 백엔드가 leidenalg에서 igraph로 바뀐다"는 안내인데, 미래 기본값을 미리 쓰려면 flavor="igraph", n_iterations=2, directed=False를 넘기라고 메시지가 직접 알려 줍니다. igraph 쪽이 더 빠르므로 위처럼 명시하는 걸 권합니다. 단 이 인자는 scanpy 1.10 이상에서만 동작합니다(구버전은 flavor를 모릅니다).

8. 마커 유전자(marker gene)와 셀타입 해석

군집을 8개로 나눴지만, 아직 각 군집이 무슨 세포인지는 모릅니다. 군집 번호(0, 1, 2, …)는 그냥 라벨일 뿐이죠. 정체를 밝히려면 각 군집에서 특이적으로 높게 발현하는 유전자(마커 유전자)를 찾고, 그게 알려진 세포 종류의 표지와 맞는지 봅니다.

# 군집별 마커 유전자 검정 (Wilcoxon 순위합 검정)
sc.tl.rank_genes_groups(adata, "leiden", method="wilcoxon")
sc.pl.rank_genes_groups(adata, n_genes=25, sharey=False)

# 결과를 데이터프레임으로 — 군집 0의 상위 마커
sc.get.rank_genes_groups_df(adata, group="0").head()
   names    scores  logfoldchanges         pvals     pvals_adj
0  LDHB   34.21...        1.06...   2.1e-200      8.3e-197
1  RPS27  31.5...         0.74...   ...           ...
2  IL7R   27.8...         1.20...   ...           ...
...

rank_genes_groups는 각 군집을 나머지 전체와 비교해, 그 군집에서 유의하게 높은 유전자를 순위로 매깁니다. method="wilcoxon" (윌콕슨 순위합 검정)이 단일세포에서 가장 널리 쓰이는 방법입니다. 결과는 sc.get.rank_genes_groups_df로 표(데이터프레임)로 꺼낼 수 있고, names·scores·logfoldchanges·pvals·pvals_adj 열을 줍니다.

이제 PBMC에서 잘 알려진 마커들을 UMAP과 닷플롯(dot plot)으로 직접 확인합니다.

# PBMC 표준 마커 유전자
marker_genes = [
    "IL7R", "CD14", "LYZ", "MS4A1", "CD8A",
    "GNLY", "NKG7", "FCGR3A", "MS4A7", "FCER1A", "CST3", "PPBP",
]
sc.pl.umap(adata, color=["leiden", "CD14", "MS4A1", "NKG7"])  # 군집 + 마커 발현
sc.pl.dotplot(adata, marker_genes, groupby="leiden")          # 마커 × 군집 닷플롯

닷플롯은 점의 색으로 평균 발현, 크기로 발현 세포 비율을 보여 줘서, 어떤 군집이 어떤 마커를 켜는지 한눈에 들어옵니다. 알려진 마커-셀타입 대응은 이렇습니다.

세포 종류 대표 마커 유전자
CD4 T세포IL7R
CD8 T세포CD8A
B세포MS4A1, CD79A
NK세포GNLY, NKG7
CD14+ 단핵구 (monocyte)CD14, LYZ
FCGR3A+ 단핵구FCGR3A, MS4A7
수지상세포 (dendritic cell)FCER1A, CST3
거대핵세포 (megakaryocyte)PPBP

각 군집의 상위 마커를 이 표와 맞춰 보고, 군집에 이름을 붙입니다.

# 군집 번호 → 셀타입 이름 (각자 데이터의 마커로 순서 확인 필수!)
new_names = [
    "CD4 T", "CD14+ Monocytes", "B", "CD8 T", "NK",
    "FCGR3A+ Monocytes", "Dendritic", "Megakaryocytes",
]
adata.rename_categories("leiden", new_names)
sc.pl.umap(adata, color="leiden", legend_loc="on data")   # 셀타입 라벨 UMAP
⚠️ 군집 번호 순서는 고정이 아니다 — 위 new_names의 순서를 그대로 베끼면 안 됩니다. 군집 번호와 셀타입의 대응은 resolution·flavor·난수 시드에 따라 달라집니다. 반드시 각 군집의 상위 rank_genes_groups 마커를 위 표와 직접 대조해 이름을 붙이세요. 라벨을 잘못 달면 그 뒤 해석이 통째로 어긋납니다.

여기까지가 단일세포 분석의 한 사이클입니다. 결과를 파일로 저장하면 다음에 이어서 쓸 수 있습니다.

adata.write("pbmc3k_processed.h5ad")   # AnnData → HDF5 파일로 저장
그림 3. 마커 × 군집 닷플롯의 원리 — 점 색=평균 발현, 점 크기=발현 세포 비율. 군집마다 켜지는 마커 조합으로 세포 종류를 판정한다.

9. 자주 발생하는 에러 & 해결법

  • ModuleNotFoundError: No module named 'leidenalg' (또는 igraph) — 입문자가 가장 많이 만나는 에러입니다. pip install scanpy만으로는 군집화 백엔드가 안 깔립니다. pip install "scanpy[leiden]" (또는 pip install igraph leidenalg)로 해결하세요. flavor="igraph"를 쓰면 igraph가, flavor="leidenalg"를 쓰면 leidenalg가 필요합니다.
  • Leiden FutureWarningflavor 없이 sc.tl.leiden을 부르면 "기본 백엔드가 igraph로 바뀐다"는 경고가 뜹니다. 에러는 아니지만, flavor="igraph", n_iterations=2, directed=False를 넘겨 미래 기본값으로 맞춰 두면 경고가 사라집니다. 단 이 인자는 scanpy 1.10 이상에서만 됩니다 — 구버전에서 넘기면 TypeError: leiden() got an unexpected keyword argument 'flavor'가 납니다.
  • igraph vs python-igraph 이름 혼동 — PyPI (pip)에서는 igraph가 정식 이름이고 python-igraph는 그쪽으로 넘겨주는 별칭입니다. 반면 conda에서는 여전히 python-igraph로 설치합니다(conda install -c conda-forge python-igraph leidenalg). 둘 다 깔려 있으면 임포트가 꼬일 수 있으니 pip 환경에선 igraph 하나만 두세요.
  • log1p 이중 적용 — 정규화 셀을 두 번 실행해 로그를 두 번 취하면 값이 망가집니다. 증상은 발현값이 비정상적으로 작게 눌리는 것입니다. scanpy가 경고로 막아 주지만, 노트북에서 셀을 반복 실행하지 않도록 주의하세요.
  • HVG flavor와 입력 데이터 불일치flavor="seurat"(기본)은 로그 정규화 데이터를, flavor="seurat_v3"는 원본 카운트를 받습니다. 잘못 넣어도 에러 없이 엉뚱한 유전자가 뽑히는 조용한 버그라 더 위험합니다. flavor와 데이터 상태를 꼭 맞추세요.
  • scanpy ↔ anndata 버전 불일치 — 오래된 anndata에 최신 scanpy를 쓰면 ImportError·AttributeError가 납니다. scanpy 1.11+는 anndata 0.10 이상이 필요합니다. pip install -U scanpy anndata로 함께 올리세요.
  • 메모리·속도 문제 — 세포가 수십만 개로 커지면 regress_out과 밀집(dense) scale이 무겁고, 수렴까지 도는 Leiden(n_iterations=-1)이 느립니다. 행렬을 희소(sparse)로 유지하고, PCA 전에 HVG로 먼저 줄이고, flavor="igraph", n_iterations=2를 쓰면 한결 가벼워집니다.

10. R Seurat과의 대응, 그리고 확장

이 글의 흐름은 R의 Seurat 분석과 사실상 같은 단계를 밟습니다. 같은 pbmc3k 데이터를 쓰니, 함수만 갈아 끼우면 두 도구를 오갈 수 있습니다.

단계 Seurat (R) scanpy (Python)
객체 생성CreateSeuratObject()AnnData / sc.read_10x_mtx()
QC 지표PercentageFeatureSet("^MT-")calculate_qc_metrics(qc_vars=["mt"])
정규화NormalizeData()normalize_total() + log1p()
고변이 유전자FindVariableFeatures()highly_variable_genes()
스케일링ScaleData()sc.pp.scale()
주성분분석RunPCA()sc.tl.pca()
이웃그래프FindNeighbors()sc.pp.neighbors()
UMAPRunUMAP()sc.tl.umap()
군집화FindClusters()sc.tl.leiden()
마커 유전자FindAllMarkers()sc.tl.rank_genes_groups()
셀타입 이름RenameIdents()adata.rename_categories()

대응이 거의 1:1입니다. 한 가지 개념적 주의는, Seurat의 vst 변동유전자 방식이 scanpy의 flavor="seurat_v3"(원본 카운트)에 해당하지, 기본값 "seurat"가 아니라는 점입니다.

여기서 더 나아가는 길도 자연스럽게 이어집니다. 셀타입 자동 주석으로는 참조 데이터에 맞춰 라벨을 붙이는 celltypist나, 마커 기반 점수를 매기는 도구들이 있습니다. 궤적 분석(trajectory)으로는 세포가 분화·활성화하며 변해가는 순서를 재구성하는 scanpy.tl.paga나 별도 패키지 scvelo(RNA velocity)·Palantir로 확장합니다. 군집이 "어떤 세포가 있나"를 답한다면, 궤적은 "어떤 순서로 변해가나"를 그립니다. scanpy 생태계(scverse)는 이런 후속 분석 도구가 같은 AnnData 위에서 맞물리도록 설계돼 있어, 한 객체를 그대로 넘기며 분석을 이어 갈 수 있습니다.

🎓 다음 학습
(R로 같은 분석) Seurat로 단일세포 RNA-seq 분석 — 같은 pbmc3k를 R로, 함수 대응 확인
(개념 복습) 단일세포 RNA 시퀀싱(scRNA-seq)이란? — 10x·바코드·UMI·드롭아웃이 무엇인지
(차원축소 직관) PCA로 RNA-seq 샘플 QC — 주성분분석이 무엇을 하는지 한 단계 깊이
(출판용 그림) ggplot2 기초 — 분석 결과를 그림으로 옮기는 그래픽 문법 (R)

References

  1. Wolf, F. A., Angerer, P., & Theis, F. J. (2018). SCANPY: large-scale single-cell gene expression data analysis. Genome Biology, 19, 15. https://doi.org/10.1186/s13059-017-1382-0 (scanpy 원논문)
  2. Virshup, I., Rybakov, S., Theis, F. J., Angerer, P., & Wolf, F. A. (2024). anndata: Annotated data. Journal of Open Source Software, 9(101), 4371. https://doi.org/10.21105/joss.04371 (anndata)
  3. Traag, V. A., Waltman, L., & van Eck, N. J. (2019). From Louvain to Leiden: guaranteeing well-connected communities. Scientific Reports, 9, 5233. https://doi.org/10.1038/s41598-019-41695-z (Leiden 알고리즘)
  4. McInnes, L., Healy, J., & Melville, J. (2018). UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction. arXiv:1802.03426. https://arxiv.org/abs/1802.03426 (UMAP)
  5. Scanpy 공식 문서 — PBMC3k 클러스터링 튜토리얼. https://scanpy.readthedocs.io/en/stable/tutorials/basics/clustering-2017.html
  6. Scanpy 공식 문서 (API). https://scanpy.readthedocs.io/en/stable/api/index.html

Pipette & Pipeline · A bio portfolio journal

이 글을 쓴 사람 Yumingming

생명융합공학과 박사과정.
Microbiome · Cosmetics · RNA Therapeutics · Bioinformatics를 공부하며,
실험(Wet Lab)과 데이터(Dry Lab)를 잇는 글을 논문(article) 기반으로 씁니다.

About · 더 알아보기 →

⚕️ 이 글은 학습·정보 제공 목적이며, 의학적 진단·치료·조언을 대체하지 않습니다. 건강·질병·치료에 관한 결정은 반드시 의사 등 전문가와 상의하세요. 자세한 내용은 면책조항을 참고해 주세요.