조직 평균은 거짓말을 한다
간을 한 조각 갈아 RNA를 뽑아 시퀀싱했다고 하자. 결과는 "간 조직의 평균 유전자 발현"이다. 그런데 그 조각 안에는 간세포, 쿠퍼세포, 담도세포, 림프구, 혈관내피세포가 뒤섞여 있다. 각 세포가 서로 다른 유전자 세트를 켜고 있다. 평균은 그 어느 세포와도 닮지 않은 유령이다.
암 조직은 더 심하다. 종양 세포도 이질적(heterogeneous)이다. 화학요법에 감수성 있는 세포와 저항성 세포가 같은 조각 안에 공존한다. 벌크 RNA-seq으로는 저항성 세포의 신호가 감수성 세포의 신호에 파묻힌다. 임상 실패의 상당수가 여기서 나온다.
단일세포 RNA-seq은 이 유령과 싸운다. 조직을 개별 세포로 분리해서 각 세포의 전사체를 따로따로 읽는다. 결과는 "간 조직 평균"이 아니라 "12,438개 개별 세포의 전사체 지도"다. 세포 이질성이 눈으로 보이기 시작한다.
어떻게 30,000개 세포를 한 번에 시퀀싱하나
가장 유명한 플랫폼은 10x Genomics Chromium이다. 원리는 놀랍도록 아름답다.
- 미세유체 칩에서 세포 · 젤 비드 · 오일 방울을 흘려보낸다.
- 각 방울(GEM: Gel Bead-in-EMulsion)에는 이상적으로 세포 하나 + 젤 비드 하나가 들어간다.
- 젤 비드에는 수백만 개의 프라이머가 붙어 있고, 비드마다 고유 바코드(cell barcode, 16bp)를 가진다.
- 세포가 방울 안에서 터지면 mRNA가 방출되고, 비드의 프라이머가 그 mRNA를 붙잡아 역전사한다. 이 때 분자마다 무작위 UMI(Unique Molecular Identifier, 12bp)가 붙는다.
- 방울을 다시 터뜨려 모아 라이브러리를 만들고 시퀀싱한다.
읽기 결과의 각 리드는 이렇게 생겼다.
[cell barcode: 16bp][UMI: 12bp][cDNA sequence: ~90bp]- cell barcode가 같은 리드들 → 같은 세포에서 온 것
- UMI가 같은 리드들 → 같은 mRNA 분자에서 온 PCR 중복
즉 UMI 덕분에 PCR 편향을 완전히 제거하고 분자 단위 정량이 가능해진다. 이게 벌크 RNA-seq에 없는 혁명이다.
Cell Ranger가 뱉는 것
10x의 공식 파이프라인 Cell Ranger가 이 원시 리드를 처리해 세 파일을 뱉는다.
matrix.mtx.gz— 유전자 × 세포 카운트 행렬 (거의 다 0인 희소 행렬)barcodes.tsv.gz— 세포 barcode 목록features.tsv.gz— 유전자 목록
이 세 파일이 우리 분석의 출발점이다. PBMC 3k라는 표준 튜토리얼 데이터셋이 10x 공식 다운로드에 있다. 세포 2,700개 · 유전자 32,738개 규모다.
AnnData — 왜 이렇게 생겼나
Scanpy(Python)와 Seurat(R)이 이 데이터를 각자의 방식으로 담는다. 이 편에서는 Scanpy의 AnnData 구조를 정면으로 뜯어본다. Seurat의 SingleCellExperiment도 개념적으로 같다.
AnnData는 다음처럼 생겼다.
adata (AnnData 객체)
├── .X → 카운트 행렬 (n_obs × n_vars, 희소)
├── .obs → 세포 메타데이터 (DataFrame, 행 = 세포)
├── .var → 유전자 메타데이터 (DataFrame, 행 = 유전자)
├── .obsm → 세포별 다차원 임베딩 (PCA, UMAP 좌표 등)
├── .varm → 유전자별 다차원 값 (PC loading 등)
├── .obsp → 세포간 거리·인접 행렬
├── .uns → 그 외 부속 정보 (색상 팔레트, 클러스터링 파라미터)
└── .layers → 대체 행렬 (raw counts, normalized, log1p …)핵심은 하나의 객체에 모든 것이 붙어 있다는 것이다. 세포를 필터링하면 .X뿐 아니라 .obs, .obsm, .obsp가 자동으로 함께 잘린다. Numpy 배열 여러 개를 손으로 관리할 때의 인덱스 지옥이 사라진다.
Scanpy로 PBMC 3k 열어보기
# Kaggle Notebook 또는 Colab에서 실행!pip install scanpy leidenalg -q
import scanpy as scimport numpy as np
# PBMC 3k 튜토리얼 데이터 자동 다운로드adata = sc.datasets.pbmc3k()print(adata)이걸 실행하면 다음이 뜬다.
AnnData object with n_obs × n_vars = 2700 × 327382,700개 세포 × 32,738개 유전자. 그리고 .X는 8억 개 셀 중 99.6%가 0인 극도의 희소 행렬이다. 이래서 sparse matrix 표현이 필수다. dense로 저장하면 700MB인데 sparse로는 40MB다.
세포와 유전자를 인덱싱하기
# 첫 5개 세포의 barcodeprint(adata.obs.head())
# 첫 5개 유전자의 정보print(adata.var.head())
# 특정 세포의 CD3D 유전자 발현adata_cell = adata[0, :] # 첫 세포cd3d_col = adata.var_names.get_loc("CD3D") # 유전자 인덱스print(adata_cell.X[0, cd3d_col])인덱싱이 pandas DataFrame처럼 자연스럽다. 그리고 adata[cells, genes] 슬라이싱이 뷰(view)를 반환하기 때문에 즉시 메모리를 복사하지 않는다. 30만 세포 규모 데이터셋에서도 대화형 탐색이 가능한 이유다.
CS 매핑
- 희소 행렬 (Sparse matrix): DryBench에서 다룬 CSR/CSC 포맷 그대로. 카운트가 0이면 저장 안 함. AnnData의
.X는 기본scipy.sparse.csr_matrix. - 다차원 인덱싱:
.obsm['X_umap']은 2,700 × 2 numpy 배열. NumPy fancy indexing이 그대로 통한다. - 뷰 vs 복사: pandas의
SettingWithCopyWarning과 같은 함정이 AnnData에도 있다. 뷰에 값을 쓰면 원본이 변한다. 안전하려면.copy(). - Zarr 저장 포맷:
adata.write_zarr()가 클라우드 친화적 저장을 제공한다. HDF5의 대안..h5ad대신.zarr폴더 구조로 저장돼 부분 로딩이 자유롭다.
이 편 이후의 여정 (Single-cell 시리즈 6편 지도)
- 다음 S27: 세포 QC — 죽어가는 세포를 어떻게 걸러내나 (mitochondrial %, doublet 검출)
- S28: 정규화 + 고변이 유전자(HVG) 선택 — 신호와 잡음 분리
- S29: 차원 축소 — PCA / t-SNE / UMAP의 기하 원리
- S30: Leiden 클러스터링 + 마커 유전자로 세포 유형 결정
- S31: 배치 보정 — 서로 다른 실험 배치를 안전하게 합치기 (Harmony, scVI)
이 6편이 완결되면 vertical (한 실험) scRNA-seq 파이프라인이 손에 들어온다. 그 다음 S32~S34에서 궤적·RNA velocity·세포간 통신(CellChat)으로 확장된다.
실무에서 자주 만나는 결함
.X가 이미 log 정규화된 값 — 원시 카운트가 필요한 함수(DESeq2 유사)에 넣으면 실패..layers['counts']백업 습관.inplace=True함정 — Scanpy 함수 대부분이 기본inplace=True. 실수로 원본 파괴.- 세포 필터 순서 실수 — 미토콘드리아 필터 → 유전자 필터 → doublet 순서가 뒤바뀌면 통계가 어그러진다. S27에서 상세히.
- 10x v2 vs v3 chemistry 혼동 — 서로 다른 세포 barcode 화이트리스트 사용. Cell Ranger 버전 명시 필수.
더 깊게 파고 싶다면
본문은 BPD가 자체 재구성한 서술이다. 심화는 아래 정통 자료를 활용하자.
- Wellcome Sanger Institute — 정보학팀의 Analysis of Single Cell RNA-seq Data (Part 1) (60분, 자막 자동번역 보통). 세계 최대 단일세포 컨소시엄이 정리한 실무 워크숍.
- Harvard STAT115 — Xiaole Shirley Liu 교수의 Intro to scRNA-seq and Preprocessing (45분, 자막 완비, 자동 번역 우수). UMI 필터링과 QC 기준의 통계적 근거.
- EMBL-EBI Training — Single-Cell Transcriptomics Analysis with Scanpy (42분, 자막 완비). AnnData 구조와 메모리 관리 팁이 밀도 있다.
- MIT 6.874 — Manolis Kellis 교수의 Single-Cell Analysis (자막 완비, 수동 자막). 딥러닝(scVI) 통합 관점.
- 참고 무료 웹북: Bioconductor Orchestrating Single-Cell Analysis with Bioconductor (bioconductor.org/books/release/OSCA). Scanpy는 아니지만 R 진영의 정석. 개념이 그대로 맵된다.
Kaggle에서 노트북 하나 열고, sc.datasets.pbmc3k()부터 시작해보자. 다음 편 S27의 QC 실습이 자연스레 이어진다.