monocle3로 단일세포 궤적·슈도타임 분석 — 세포가 변해가는 길을 그리다 (R 실전)

TL;DR — Seurat로 군집화까지 끝내면 "어떤 세포가 몇 종류 있나"는 답했지만, 분화나 활성화처럼 세포가 어떤 순서로 변해가는지는 아직 모릅니다. 이걸 재구성하는 도구가 R 패키지
monocle3입니다. 세포 상태의 변화를 잇는 궤적(trajectory)을 학습하고, 시작점 기준으로 진행 정도를 매기는 슈도타임(pseudotime)을 계산합니다.흐름은
cell_data_set(CDS) 만들기 →preprocess_cds(PCA) →reduce_dimension(UMAP) →cluster_cells→learn_graph(주그래프 학습) →order_cells(루트 지정 → 슈도타임) →plot_cells로 색칠하기입니다. 마지막에 슈도타임을 따라 변하는 유전자를graph_test로 찾습니다.군집(cluster)이 세포를 이산적인 그룹으로 나눈다면, 궤적은 그 사이의 연속적인 변화를 그립니다. 분화 단계처럼 경계가 흐릿하게 이어지는 과정엔 궤적이 더 맞습니다.
🔗 관련 글 · 그 전 단계인 군집화는 Seurat로 단일세포 RNA-seq 분석 · 궤적 유전자를 히트맵으로 보려면 ComplexHeatmap 히트맵 · 출판용 그림 문법은 ggplot2 기초
이 글에서 할 것
Seurat 글에서 단일세포 RNA-seq (scRNA-seq) 데이터를 품질관리(QC)하고 군집으로 나눈 뒤 셀타입까지 붙였습니다. 그 다음 자연스럽게 떠오르는 질문이 있습니다. "이 세포들이 정지 상태로 흩어져 있는 게 아니라, 어떤 순서로 변해가는 중이라면 그 경로는 어떻게 생겼을까?" 줄기세포에서 성숙 세포로 가는 분화, 면역세포의 활성화처럼 연속적인 상태 변화를 다룰 때 던지는 질문입니다.
여기에 답하는 두 개념이 궤적(trajectory)과 슈도타임(pseudotime)입니다. 궤적은 세포 상태의 변화를 연결한 그래프입니다. 차원축소 공간에서 비슷한 세포끼리 이웃하도록 펼친 뒤, 그 위에 변화의 흐름을 따라가는 곡선(주그래프, principal graph)을 얹습니다. 슈도타임은 그 곡선 위에서 시작점으로부터 각 세포가 얼마나 진행했는지를 나타내는 값입니다. 이름에 '타임'이 들어가지만 실제 시간(분·시간·일)이 아니라 진행 정도의 순서라는 점이 핵심입니다. 세포를 한 시점에 한꺼번에 잡아도, 그 안에 분화의 여러 단계가 섞여 있으면 그 단계를 한 줄로 세울 수 있다는 발상입니다.
이 글은 monocle3 (Cao et al. 2019)로 작은 더미 데이터를 만들어 CDS를 구성하고, 전처리·차원축소·군집화를 거쳐 주그래프를 학습하고, 루트(시작 세포)를 지정해 슈도타임을 매기고, 슈도타임으로 색칠한 궤적을 그립니다. 마지막에 슈도타임을 따라 발현이 변하는 유전자를 graph_test로 찾고, 군집과 궤적의 차이, 흔한 실수를 정리합니다.
# 핵심 흐름 한눈에 (monocle3)
library(monocle3)
cds <- new_cell_data_set(expr, cell_metadata, gene_metadata) # ① CDS 생성
cds <- preprocess_cds(cds, num_dim = 50) # ② PCA
cds <- reduce_dimension(cds) # ③ UMAP
cds <- cluster_cells(cds) # ④ 군집·파티션
cds <- learn_graph(cds) # ⑤ 주그래프 학습
cds <- order_cells(cds, root_pr_nodes = root) # ⑥ 루트 → 슈도타임
plot_cells(cds, color_cells_by = "pseudotime") # ⑦ 색칠
1. 사전 준비 — 설치와 입력 데이터
monocle3은 CRAN이 아니라 GitHub에서 설치합니다. 의존성으로 Bioconductor 패키지 몇 개(BiocGenerics·SingleCellExperiment 등)가 먼저 필요합니다.
# Bioconductor 의존성 먼저
if (!requireNamespace("BiocManager", quietly = TRUE))
install.packages("BiocManager")
BiocManager::install(c("BiocGenerics", "DelayedArray", "DelayedMatrixStats",
"limma", "S4Vectors", "SingleCellExperiment",
"SummarizedExperiment", "batchelor"))
# monocle3 본체 (GitHub)
install.packages("devtools")
devtools::install_github("cole-trapnell-lab/monocle3")
monocle3의 기본 자료구조는 cell_data_set (CDS)입니다. 만드는 데 필요한 건 세 가지입니다. 발현 행렬(유전자 × 세포, 보통 희소행렬), 세포 정보를 담은 cell_metadata (행=세포), 유전자 정보를 담은 gene_metadata (행=유전자)입니다. 여기서는 설명용으로 분화의 세 단계(초기·중간·말기)를 흉내 낸 작은 더미 데이터를 만듭니다.
library(monocle3)
set.seed(42) # 재현성 고정
n_genes <- 200
n_cells <- 600 # 단계당 200세포 × 3단계
# 잠재 진행도(latent progression) 0→1 을 따라 세포를 배치
progress <- sort(runif(n_cells)) # 0~1 사이 진행도
# 발현 행렬: 일부 유전자는 진행도에 따라 증가/감소하도록 설계
expr <- matrix(rpois(n_genes * n_cells, lambda = 1),
nrow = n_genes, ncol = n_cells)
# 앞쪽 30개 유전자는 진행도가 커질수록 발현↑ (분화 마커 흉내)
for (g in 1:30)
expr[g, ] <- rpois(n_cells, lambda = 1 + 8 * progress)
# 다음 30개 유전자는 진행도가 커질수록 발현↓ (초기 마커 흉내)
for (g in 31:60)
expr[g, ] <- rpois(n_cells, lambda = 1 + 8 * (1 - progress))
rownames(expr) <- paste0("gene", seq_len(n_genes))
colnames(expr) <- paste0("cell", seq_len(n_cells))
# 세포 메타데이터: 진행도 구간으로 3단계 라벨
cell_meta <- data.frame(
row.names = colnames(expr),
stage = cut(progress, breaks = c(-Inf, 0.33, 0.66, Inf),
labels = c("초기", "중간", "말기"))
)
# 유전자 메타데이터: gene_short_name 열은 필수 (plot_cells가 참조)
gene_meta <- data.frame(
row.names = rownames(expr),
gene_short_name = rownames(expr)
)
cds <- new_cell_data_set(expression_data = expr,
cell_metadata = cell_meta,
gene_metadata = gene_meta)
cds
class: cell_data_set
dim: 200 600
assays(1): counts
rownames(200): gene1 gene2 ... gene199 gene200
colnames(600): cell1 cell2 ... cell599 cell600
colData names(2): stage Size_Factor
gene_metadata에 gene_short_name 열을 넣은 건 빠뜨리기 쉬운 필수 조건입니다. plot_cells가 유전자를 이름으로 찾을 때 이 열을 참조하므로, 없으면 나중에 에러가 납니다. 실제 분석이라면 발현 행렬은 Read10X로 읽은 카운트, cell_metadata는 샘플·조건·군집 정보가 됩니다.
SeuratWrappers의 as.cell_data_set()이 Seurat 객체를 CDS로 바꿔 줍니다. cds <- SeuratWrappers::as.cell_data_set(seurat_obj) 한 줄이면 됩니다. Seurat 글에서 만든 객체를 그대로 이어받아 이 글의 learn_graph 단계로 바로 넘어갈 수 있습니다.2. 전처리와 차원축소 — preprocess_cds → reduce_dimension
CDS가 준비되면 주성분분석(PCA)으로 차원을 줄이고(preprocess_cds), 그 결과를 UMAP으로 2차원에 펼칩니다(reduce_dimension). Seurat의 RunPCA·RunUMAP에 해당하는 단계입니다.
# ② 전처리: 정규화 + PCA (num_dim = 사용할 주성분 수)
cds <- preprocess_cds(cds, num_dim = 50)
# (선택) 배치효과가 있으면 여기서 보정
# cds <- align_cds(cds, alignment_group = "batch")
# ③ 차원축소: UMAP (기본 reduction_method = "UMAP")
cds <- reduce_dimension(cds)
# 단계 라벨로 색칠해 UMAP 확인
plot_cells(cds, color_cells_by = "stage",
label_cell_groups = FALSE,
show_trajectory_graph = FALSE) # 아직 궤적 학습 전
num_dim은 PCA에서 몇 개의 주성분을 쓸지 정합니다. 너무 작으면 변화의 축을 놓치고, 너무 크면 잡음까지 끌어와 군집·궤적이 흔들립니다. 실제 데이터에서는 plot_pc_variance_explained(cds)로 분산 설명 곡선이 꺾이는 지점을 보고 정합니다. 배치(batch)가 섞여 있다면 align_cds로 보정한 뒤 차원축소를 해야 합니다. 보정을 건너뛰면 생물학적 변화가 아니라 배치 차이가 궤적으로 잡히는 함정에 빠집니다.
3. 군집화 — cluster_cells
cluster_cells는 UMAP 공간에서 세포를 군집(cluster)으로 나누고, 동시에 파티션(partition)이라는 더 큰 단위로도 묶습니다. 파티션은 서로 떨어진 세포 덩어리로, monocle3는 파티션마다 별도의 궤적을 학습합니다. 즉 연결되지 않은 세포 집단을 억지로 한 궤적에 잇지 않습니다.
# ④ 군집화 (resolution 으로 군집 해상도 조절)
cds <- cluster_cells(cds, resolution = 1e-2)
# 군집으로 색칠
plot_cells(cds, color_cells_by = "cluster",
show_trajectory_graph = FALSE)
# 파티션 확인 (궤적이 그려지는 단위)
plot_cells(cds, color_cells_by = "partition",
show_trajectory_graph = FALSE)
resolution을 키우면 군집이 잘게 쪼개지고, 줄이면 뭉칩니다. 군집 해상도가 너무 높으면 하나의 연속 분화가 여러 조각으로 끊겨 궤적이 부자연스러워지고, 너무 낮으면 서로 다른 경로가 한 덩어리로 뭉쳐 분기(branch)를 놓칩니다. 데이터에 맞춰 몇 번 조정하는 게 보통입니다.
4. 주그래프 학습 — learn_graph
이제 궤적의 뼈대를 학습합니다. learn_graph는 세포가 펼쳐진 공간 위로 변화의 흐름을 따라가는 주그래프(principal graph)를 적합합니다. 이 곡선이 "어디서 시작해 어디로 갈라지고 어디로 끝나는지"의 골격입니다.
# ⑤ 주그래프 학습
cds <- learn_graph(cds)
# 궤적(주그래프)을 단계 라벨 위에 겹쳐 그리기
plot_cells(cds,
color_cells_by = "stage",
label_groups_by_cluster = FALSE,
label_leaves = TRUE, # 가지 끝(잎) 표시
label_branch_points = TRUE) # 분기점 표시
그림에 세포 위로 검은 선이 얹히는데, 이게 주그래프입니다. 끝점(잎, leaf)은 분화의 종착 상태, 분기점(branch point)은 한 경로가 둘 이상으로 갈리는 지점입니다. 아직 이 그래프는 방향이 없습니다. 어느 끝이 시작이고 어느 끝이 마지막인지는 다음 단계에서 사람이 지정해야 합니다.
5. 루트 지정과 슈도타임 — order_cells
궤적에 방향을 주는 단계입니다. order_cells에 루트(root, 시작 지점)를 알려 주면, 그 지점에서부터 그래프를 따라간 거리로 각 세포의 슈도타임이 정해집니다. 루트를 지정하지 않으면 슈도타임의 기준점이 없어 값 자체가 의미를 잃습니다 — 가장 중요한 한 단계입니다.
# 루트 주그래프 노드를 프로그램으로 고르는 헬퍼
# (여기서는 'stage == 초기' 세포가 가장 많은 노드를 시작점으로)
get_root_node <- function(cds, start_stage = "초기") {
cell_ids <- which(colData(cds)$stage == start_stage)
closest <- igraph::V(principal_graph(cds)$UMAP)$name[
as.numeric(names(which.max(table(
principal_graph_aux(cds)$UMAP$pr_graph_cell_proj_closest_vertex[cell_ids, ]
))))
]
closest
}
# ⑥ 루트 지정 → 슈도타임 계산
cds <- order_cells(cds, root_pr_nodes = get_root_node(cds))
# ⑦ 슈도타임으로 색칠 (연속 색 = 진행 정도)
plot_cells(cds,
color_cells_by = "pseudotime",
label_leaves = FALSE,
label_branch_points = FALSE)
RStudio에서 인자 없이 order_cells(cds)만 실행하면 마우스로 루트를 클릭하는 창이 뜹니다. 탐색할 땐 편하지만, 재현 가능한 분석에는 위처럼 root_pr_nodes로 시작 노드를 코드로 못박는 편이 낫습니다. 슈도타임으로 색칠한 그림에서 색이 한쪽 끝(루트)에서 반대쪽으로 매끄럽게 번지면, 세포들이 그 방향으로 진행하는 연속 과정으로 잘 정렬된 것입니다. 각 세포의 슈도타임 값은 pseudotime(cds)로 꺼내 다른 분석에 쓸 수 있습니다.
monocle3는 어느 끝이 시작인지 스스로 알지 못합니다. 분화라면 줄기·전구세포 마커가 높은 쪽, 활성화라면 정지(naive) 상태가 루트입니다. 마커 유전자 발현이나 이미 아는 세포 상태를 근거로 루트를 고르세요. 루트를 반대로 잡으면 슈도타임이 통째로 뒤집혀 "역방향 분화" 같은 잘못된 해석이 나옵니다.
6. 군집(이산) vs 궤적(연속), 그리고 슈도타임 유전자
같은 데이터라도 군집과 궤적은 보는 각도가 다릅니다. 둘은 경쟁이 아니라 보완 관계입니다.
| 구분 | 군집(cluster) | 궤적(trajectory)·슈도타임 |
|---|---|---|
| 보는 구조 | 이산(discrete) — 떨어진 그룹 | 연속(continuous) — 이어지는 변화 |
| 핵심 질문 | 어떤 세포 종류가 있나 | 어떤 순서로 변해가나 |
| 출력 | 군집 라벨(범주) | 슈도타임 값(연속) + 분기 |
| 색칠 | 범주형 색(군집별) | 연속 그라데이션(진행 정도) |
| 적합한 상황 | 뚜렷이 구분되는 세포 유형 | 분화·활성화처럼 단계가 이어짐 |
| monocle3 함수 | cluster_cells | learn_graph · order_cells |
궤적을 손에 쥐었으면 다음 질문은 "슈도타임을 따라 발현이 변하는 유전자는 무엇인가"입니다. graph_test가 주그래프 상에서 공간적으로 자기상관(autocorrelation)이 큰 유전자, 즉 궤적의 특정 구간에 발현이 몰리는 유전자를 찾아 줍니다. 통계량은 모런 지수(Moran's I)입니다.
# 주그래프를 따라 발현이 변하는 유전자 검정 (Moran's I)
deg_res <- graph_test(cds, neighbor_graph = "principal_graph", cores = 4)
# 유의한 유전자만: morans_I 가 클수록 궤적을 따라 강하게 변함
sig_genes <- subset(deg_res, q_value < 0.05)
sig_genes <- sig_genes[order(-sig_genes$morans_I), ]
head(sig_genes[, c("gene_short_name", "morans_I", "q_value")])
# 상위 유전자를 슈도타임 궤적 위에 발현으로 표시
plot_cells(cds,
genes = head(rownames(sig_genes), 4),
show_trajectory_graph = FALSE,
label_cell_groups = FALSE,
label_leaves = FALSE)
gene_short_name morans_I q_value
gene7 gene7 0.6831042 3.10e-58
gene42 gene42 0.6552180 1.22e-54
gene15 gene15 0.6310977 8.04e-51
gene38 gene38 0.5994461 2.71e-47
여기서 모런 지수(Moran's I)는 −1에서 +1 사이이고, 값이 클수록 그 유전자가 궤적의 특정 구간에 발현이 집중된다는 뜻입니다. 더미 데이터에서 진행도에 따라 증가·감소하도록 심어 둔 앞쪽 유전자들이 상위에 올라옵니다. 실제 분석이라면 이 유전자 목록을 슈도타임 순으로 정렬해 ComplexHeatmap으로 그리면, 분화 단계마다 켜지고 꺼지는 유전자 프로그램이 한눈에 보입니다.

7. 자주 발생하는 에러 & 오해
- 루트를 지정하지 않음 → 슈도타임이 무의미 —
order_cells에root_pr_nodes(또는root_cells)를 주지 않으면 기준점이 없어 슈도타임 값이 뒤죽박죽이 됩니다. 시작 세포는 알고리즘이 아니라 생물학(줄기·전구세포 마커, 정지 상태)으로 정해 코드로 못박으세요. - monocle2 API와 혼동 —
monocle3는 이전 버전(monocle2)과 함수가 다릅니다.newCellDataSet·differentialGeneTest·reduceDimension(method = "DDRTree")는 monocle2 문법이라monocle3에서는 동작하지 않습니다.monocle3에서는new_cell_data_set·graph_test·reduce_dimension을 씁니다. gene_short_name열 누락 —gene_metadata에gene_short_name열이 없으면plot_cells(genes = ...)가 유전자를 못 찾아 에러를 냅니다. CDS를 직접 만들 땐 이 열을 꼭 넣으세요.- 배치효과를 보정하지 않음 — 여러 샘플·실험 배치가 섞였는데
align_cds로 보정하지 않으면, 생물학적 변화가 아니라 배치 차이가 궤적으로 잡힙니다.reduce_dimension전에align_cds(cds, alignment_group = "batch")로 정렬하세요. - 군집 해상도를 방치 —
cluster_cells의resolution이 너무 높으면 연속 분화가 토막 나고, 너무 낮으면 분기를 놓칩니다. 궤적이 어색하면 해상도부터 조정해 보세요. - 슈도타임을 실제 시간으로 오해 — 슈도타임은 시작점 기준의 상대적 진행 순서일 뿐, 분·시간 같은 물리적 시간이 아닙니다. 두 세포의 슈도타임 차이가 실제 경과 시간에 비례하지 않습니다.
• (그 전 단계) Seurat로 단일세포 RNA-seq 분석 — QC·군집·셀타입까지, 이 글의 입력을 만드는 단계
• (궤적 유전자 시각화) ComplexHeatmap 히트맵 — 슈도타임 순으로 변하는 유전자를 한 장에
• (개념 복습) 단일세포 RNA 시퀀싱(scRNA-seq)이란? — scRNA-seq가 무엇을 보는지
• (출판용 그림) ggplot2 기초 · DESeq2로 차등발현 분석

References
- Cao, J., Spielmann, M., Qiu, X. et al. (2019). The single-cell transcriptional landscape of mammalian organogenesis. Nature, 566, 496–502. (monocle3 원논문)
- Trapnell, C., Cacchiarelli, D., Grimsby, J. et al. (2014). The dynamics and regulators of cell fate decisions are revealed by pseudotemporal ordering of single cells. Nature Biotechnology, 32, 381–386. (슈도타임 개념)
- Qiu, X., Mao, Q., Tang, Y. et al. (2017). Reversed graph embedding resolves complex single-cell trajectories. Nature Methods, 14, 979–982. (주그래프 학습)
- Street, K. et al. (2018). Slingshot: cell lineage and pseudotime inference for single-cell transcriptomics. BMC Genomics, 19, 477. (대안 궤적 도구)
Pipette & Pipeline · A bio portfolio journal
'Code Bench > R Snippets' 카테고리의 다른 글
| pROC로 ROC 곡선·AUC 그리기 — 바이오마커·예측 모델 성능 평가 (R 실전) (0) | 2026.06.22 |
|---|---|
| GSVA로 샘플별 경로 활성 점수 매기기 — 유전자에서 경로로 (R 실전) (0) | 2026.06.19 |
| R로 기초 통계 검정하기 — t-검정·ANOVA·상관·비모수 (R 실전) (0) | 2026.06.18 |
| pheatmap으로 발현 히트맵 빠르게 그리기 — z-score·클러스터링·주석 (R 실전) (0) | 2026.06.18 |
| dplyr·tidyr로 생물 데이터 정리하기 — 분석 전 90%는 데이터 정리 (R 실전) (0) | 2026.06.18 |



































