BioPlayground

🧬
목록으로

발현 클러스터링 — 상관관계와 정렬로 유전자 co-expression 그룹 찾기

5,000개 유전자를 함께 움직이는 그룹으로 묶는 계층적 클러스터링을 numpy와 상관관계 행렬로 직접 구현. scikit-learn 뒤에 있는 원리를 코드로 이해.

중급
|
90
|
검증 완료 (2026-07)
co-expression유전자 클러스터링hierarchical clusteringPearson correlation발현 matrixgene moduleheatmap
진행률0/10 (0%)

발현 클러스터링 — 상관관계와 정렬로 유전자 co-expression 그룹 찾기

이 토픽을 마치면

교과서에서 배운 numpy, 상관관계, 정렬 을 엮어서, 여러 샘플에 걸친 유전자 발현 데이터에서 함께 움직이는(co-expression) 그룹을 찾는 계층적 클러스터링을 직접 구현할 수 있습니다. scikit-learn이나 scipy.cluster가 뒤에서 하는 일의 원리를 코드로 이해합니다.

이 글은 교육용 일반 예제 입니다. 실전 클러스터링은 pandas/scanpy/scikit-learn을 씁니다. 여기서는 이 도구들의 내부 개념을 정확히 다룹니다.


"이 5,000개 유전자 중 어떤 게 함께 움직이지?" — 눈으로는 못 본다

여러분에게 RNA-seq 데이터가 있습니다. 5,000 유전자 × 30 샘플의 발현 행렬. 다음 질문을 던집니다.

  • 어떤 유전자들이 함께 켜지고 꺼지는가?
  • 이 co-expression 그룹이 어떤 생물학적 기능에 대응하는가?

5,000개 유전자의 30차원 벡터를 눈으로 비교하기는 불가능합니다. 자동으로 관계를 찾아 정렬해야 합니다.

순진한 접근: 유전자별로 발현 프로필을 그린다. 5,000개 그래프. 어차피 관계는 안 보임.

진짜 접근: 두 축이 있습니다.

  1. 상관관계: 두 유전자의 발현 프로필이 얼마나 함께 움직이는지 정량. Pearson correlation.
  2. 계층적 클러스터링: 가장 유사한 쌍부터 그룹으로 병합. 결과가 dendrogram(계통수 유사).

이 둘을 조합하면 함께 움직이는 유전자 그룹들이 자연스러운 계층 구조로 나타납니다.


블랙박스에서 부품으로

부품 1: 발현 행렬과 상관관계

python
import numpy as np
def load_expression_matrix(path: str) -> tuple[np.ndarray, list[str], list[str]]:
"""
반환: (matrix, gene_names, sample_names)
matrix shape: (n_genes, n_samples)
"""
with open(path) as f:
header = f.readline().strip().split("\t")
sample_names = header[1:]
gene_names = []
rows = []
for line in f:
parts = line.strip().split("\t")
gene_names.append(parts[0])
rows.append([float(x) for x in parts[1:]])
return np.array(rows), gene_names, sample_names
expr, genes, samples = load_expression_matrix("expression.tsv")
print(f"유전자 {len(genes)}, 샘플 {len(samples)}")
print(f"행렬 shape: {expr.shape}")

이제 상관관계 행렬. 각 원소는 두 유전자 사이의 Pearson correlation.

python
def compute_correlation_matrix(expr: np.ndarray) -> np.ndarray:
"""
expr shape: (n_genes, n_samples)
반환: (n_genes, n_genes) 상관관계 행렬
"""
return np.corrcoef(expr)
corr = compute_correlation_matrix(expr)
print(f"corr shape: {corr.shape}") # (5000, 5000)
print(f"대각선: {corr[0, 0]}") # 1.0 (자기 자신)

np.corrcoef 는 각 행을 하나의 변수로 취급합니다. 즉 expr(유전자, 샘플) 형태면 결과는 유전자-유전자 상관.

부품 2: 거리 행렬

클러스터링에서는 유사도(similarity) 대신 거리(distance) 를 씁니다. 상관관계는 -1~+1의 유사도라, 다음 변환으로 거리로 만듭니다.

python
def correlation_to_distance(corr: np.ndarray) -> np.ndarray:
"""
거리 = 1 - correlation.
양의 상관 강함 → 거리 작음.
음의 상관 (반대로 움직임) → 거리 큼.
"""
return 1.0 - corr
dist = correlation_to_distance(corr)

주의: 다른 정의도 있습니다. sqrt(2 * (1 - corr)) 는 유클리드 거리와 일관성 있는 정의. 목적에 따라 선택.

부품 3: 계층적 클러스터링 (Single Linkage)

Agglomerative 접근: 처음엔 각 유전자가 별개 클러스터. 거리가 가장 가까운 두 클러스터를 병합하는 것을 반복. 하나의 클러스터가 남을 때까지.

두 클러스터 사이의 거리를 정의해야 합니다. Single linkage 는 가장 단순한 정의: 두 클러스터의 원소 쌍 중 최소 거리.

python
def hierarchical_clustering_single(dist: np.ndarray) -> list[tuple[int, int, float]]:
"""
반환: 병합 이벤트 리스트 [(cluster_a, cluster_b, merge_distance), ...]
각 병합 후 새 클러스터 id는 원래 개수 + 이벤트 인덱스로 부여.
"""
n = dist.shape[0]
active_clusters = {i: [i] for i in range(n)}
cluster_distances = {(i, j): dist[i, j] for i in range(n) for j in range(i + 1, n)}
events: list[tuple[int, int, float]] = []
next_id = n
while len(active_clusters) > 1:
best_pair = min(cluster_distances, key=cluster_distances.get)
i, j = best_pair
merge_dist = cluster_distances[best_pair]
events.append((i, j, merge_dist))
new_cluster = active_clusters[i] + active_clusters[j]
del active_clusters[i]
del active_clusters[j]
active_clusters[next_id] = new_cluster
# 새 클러스터와 나머지 클러스터의 거리 계산 (single linkage = min)
new_distances = {}
for existing_id in active_clusters:
if existing_id == next_id:
continue
existing = active_clusters[existing_id]
min_d = min(
dist[a, b] for a in new_cluster for b in existing
)
new_distances[(min(existing_id, next_id), max(existing_id, next_id))] = min_d
cluster_distances = {
k: v for k, v in cluster_distances.items()
if i not in k and j not in k
}
cluster_distances.update(new_distances)
next_id += 1
return events

시간 복잡도: 순진한 구현으로 O(n³). 5,000개 유전자면 감당하기 어렵고, 실전은 O(n² log n) 알고리즘을 씁니다. 이 튜토리얼은 개념 이해가 목적이므로 순진한 구현.

부품 4: 결과에서 클러스터 추출

병합 이벤트 리스트에서 특정 개수의 클러스터를 뽑습니다.

python
def cut_dendrogram(events: list[tuple[int, int, float]], n_leaves: int, num_clusters: int):
parent = list(range(n_leaves + len(events)))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(x, y, new_id):
rx, ry = find(x), find(y)
parent[rx] = new_id
parent[ry] = new_id
# 마지막 (num_clusters - 1) 개의 병합만 무시하고 그 앞까지 union
events_to_apply = events[:-num_clusters + 1] if num_clusters > 1 else events
next_id = n_leaves
for a, b, _ in events_to_apply:
union(a, b, next_id)
next_id += 1
clusters = {}
for leaf in range(n_leaves):
root = find(leaf)
clusters.setdefault(root, []).append(leaf)
return list(clusters.values())
cluster_lists = cut_dendrogram(events, n_leaves=len(genes), num_clusters=10)
for i, members in enumerate(cluster_lists):
print(f"클러스터 {i}: {len(members)} 유전자")

Union-Find 자료구조로 O(α(n)) ≈ O(1) 병합·조회. 큰 규모에서도 유용합니다.


Sorting과 정렬된 heatmap

클러스터링의 시각적 성과는 재정렬된 heatmap에서 나타납니다. 각 클러스터의 유전자들이 인접한 행이 되도록 정렬합니다.

python
def cluster_order(cluster_lists: list[list[int]]) -> list[int]:
"""각 클러스터 안에서 원래 인덱스 순서를 유지하며, 클러스터별로 정렬."""
result = []
for members in cluster_lists:
result.extend(sorted(members))
return result
new_order = cluster_order(cluster_lists)
reordered_expr = expr[new_order]

이 재정렬된 행렬을 heatmap으로 그리면 각 클러스터가 명확한 블록으로 보입니다.

python
import matplotlib.pyplot as plt
def plot_heatmap(matrix: np.ndarray, ax=None) -> None:
if ax is None:
_, ax = plt.subplots(figsize=(6, 8))
im = ax.imshow(matrix, cmap="RdBu_r", aspect="auto")
plt.colorbar(im, ax=ax)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 8))
plot_heatmap(expr, ax1)
ax1.set_title("Original")
plot_heatmap(reordered_expr, ax2)
ax2.set_title("Clustered")
plt.tight_layout()
plt.show()

이 재정렬 전/후 대비가 클러스터링의 힘을 눈으로 확인시키는 결정적 시각화입니다.


페이딩 — 여러분이 채워야 할 세 빈칸

빈칸 1: Complete linkage / Average linkage

Single linkage 대신 complete linkage (두 클러스터 원소 쌍 중 최대 거리) 또는 average linkage (평균 거리)를 구현.

python
def hierarchical_clustering_complete(dist: np.ndarray) -> list[tuple[int, int, float]]:
# single linkage 코드와 대부분 동일, min → max 로 교체
# ...
# 클러스터간 거리 계산 부분:
# TODO: min(dist[a, b] for ...) 를 max(dist[a, b] for ...) 로
pass

힌트: 함수 하나만 파라미터화. linkage: Callable[[list[float]], float] = min or max or (lambda xs: sum(xs) / len(xs)).

빈칸 2: 클러스터의 대표 유전자

각 클러스터에서 다른 멤버들과 평균 상관관계가 가장 높은 유전자 = 대표 유전자.

python
def find_hub_genes(
corr: np.ndarray,
clusters: list[list[int]],
gene_names: list[str]
) -> list[tuple[str, float]]:
"""각 클러스터의 대표 유전자와 그 평균 상관관계."""
hubs = []
for members in clusters:
# TODO: 각 멤버에 대해 다른 멤버들과의 평균 상관관계 계산
# 가장 높은 값을 가진 유전자를 hub로 선정
pass
return hubs

힌트:

python
best_score = -1
best_gene = None
for m in members:
score = np.mean([corr[m, other] for other in members if other != m])
if score > best_score:
best_score = score
best_gene = gene_names[m]

빈칸 3: functional annotation (외부 도구 연동)

클러스터의 유전자 리스트를 KEGG pathway나 GO term과 매핑. 여기선 CSV 파일에서 매핑을 로드.

python
def enrich_clusters(
clusters: list[list[str]],
gene_to_pathway_csv: str
) -> dict:
"""
각 클러스터에서 어떤 pathway가 가장 많이 나타나는지 상위 3개.
"""
# TODO: CSV 로드 (gene, pathway 컬럼)
# 각 클러스터에서 pathway 등장 빈도 계산
# 각 클러스터의 상위 3 pathway 반환
pass

힌트: from collections import Counter; counter = Counter(); for g in members: counter.update(gene_pathways.get(g, [])).


성찰 — 실전 co-expression 도구와의 차이

scipy.cluster.hierarchy: 실전은 이 모듈의 linkage 함수를 씁니다. C로 짜인 O(n² log n) 알고리즘. 여러분의 O(n³) 순진한 구현 대비 100배 이상 빠릅니다.

WGCNA: 유전자 co-expression 네트워크의 표준 도구. Signed adjacency matrix, soft thresholding, module preservation 등의 정교한 개념을 씁니다. 여러분의 도구는 이것의 최소 뼈대.

scanpy: 단일 세포 RNA-seq 표준 스택. 클러스터링은 대개 Leiden algorithm — graph-based approach로, 여러분이 다룬 계층적 클러스터링과는 다른 계열.

Batch effect: 실전 발현 데이터는 다른 실험 배치에서 온 데이터를 합쳐야 합니다. Batch effect가 진짜 co-expression 시그널을 오염시킵니다. Combat, Harmony 같은 도구가 이 문제 해결.

동적 트리 자르기: 여러분의 cut_dendrogram 은 클러스터 개수를 지정. 실전 WGCNA는 dendrogram 모양을 보고 동적으로 자르는 알고리즘을 씁니다 (Dynamic Tree Cut).


확장 프로젝트

1. scipy로 재구현: 여러분의 순진한 클러스터링을 scipy.cluster.hierarchy.linkage 로 대체해 성능 비교.

2. dendrogram 시각화: scipy.cluster.hierarchy.dendrogram 으로 트리 시각화. 여러분의 events 리스트에서 scipy 형식으로 변환.

3. GO term enrichment: gseapy 로 각 클러스터의 GO term enrichment를 실제 수행.

4. Streamlit 대시보드: 사용자가 클러스터 개수를 슬라이더로 조정하면 heatmap과 대표 유전자가 실시간 갱신.


이 편의 부품 지도

  • [F] numpy: 발현 행렬, 상관관계 행렬 조작. np.corrcoef 실전 활용.
  • [F] 상관관계: Pearson correlation을 두 유전자 유사도의 정량 기준으로 사용.
  • [F] 정렬: 클러스터 재정렬로 heatmap 블록화. Union-Find로 결과 추출.
  • [W] matplotlib: heatmap 시각화 (완성 스크립트 제공).

[F] = 여러분이 직접 구현 / [W] = 완성 코드로 제공.