지난 이야기: 아 염색체별로 CLNSIG 비중이 이렇구나
Pathogenic 일로와봐
pathogenic_df = clinvar_df.filter(pl.col('CLNSIG_Group') == 'Pathogenic')
이렇게 하면 됩니다.
CLNVC별로 보기
clnvc_grp = clinvar_df.group_by('CLNVC').agg(
pl.col('CLNSIG').count().alias("Total")
).sort('Total', descending=True)
묶어드렸습니다^^
fig = go.Figure()
fig.add_trace(
go.Bar(x = clnvc_grp['CLNVC'], y = clnvc_grp['Total'], marker_color = px.colors.sequential.algae, text = clnvc_grp['Total'])
)
fig.update_layout(
width = 1200, height = 800,
xaxis=dict(title='Pathogenic Variant Count', tickangle = -90),
yaxis=dict(title='Count'),
title = "ClinVar Group Distribution",
margin = dict(t=50, l=10, r=10, b=10)
)
# fig.write_image('pathogenic_distributuion.png')
fig.show()

SNV가 너무 압도적인데...?
fig = px.treemap(
clnvc_grp,
path = ['CLNVC'],
values = 'Total',
color = 'Total',
color_continuous_scale = 'algae',
title = "Pathogenic Variant Distribution",
)
# text_auto 대신 update_traces를 사용하여 블록 안에 텍스트를 강제로 넣습니다.
fig.update_traces(
# label(그룹명)과 value(숫자)를 함께 표시하라는 명령입니다.
textinfo = "label+value",
# 숫자 포맷을 지정합니다 (천 단위 콤마: ,d)
texttemplate = "%{label}<br>%{value:,d}",
# 텍스트가 블록 크기에 맞게 조절되도록 설정
textfont_size = 20,
insidetextfont_family = "NanumSquare_ac" # 설정하신 폰트 적용
)
fig.update_layout(
width = 1200,
height = 800,
margin = dict(t=50, l=10, r=10, b=10)
)
# fig.write_image('pathigenic_distributuion_heatmap.png')
fig.show()

SNV 면적이 압도적이죠?
fig = go.Figure()
fig.add_trace(go.Pie(
labels = clnvc_grp['CLNVC'],
values = clnvc_grp['Total'],
textinfo = 'label+percent', # 이름과 퍼센트 동시에 표시
insidetextorientation = 'radial',
marker = dict(colors=px.colors.qualitative.Vivid) # 전역 설정 색감 유지
))
fig.update_layout(
title_text = "Pathogenic Variant Distribution", title_x=0.9,
width = 1200,
height = 800,
margin = dict(t=50, l=20, r=20, b=10)
)
# fig.write_image('pathgenic_distributuion_pie.png')
fig.show()

음… 이거 포폴에 써먹을 수 있나…
상염색체가 쪽수가 많은 건 알겠는데, 뭐가 제일 많음?
# 1. 데이터에 있는 실제 값 순서 (가출 방지용)
real_values = ["Sex_Chrom", "Autosome", "Mitochondria"]
# 2. 그래프 위에 표시하고 싶은 예쁜 제목들 (순서 일치 필수!)
display_titles = ["Sex Chromosome", "Autosome", "Mitochondria"]
# 3. 서브플롯 생성
fig = make_subplots(
rows=1, cols=3,
subplot_titles=display_titles, # 여기서 예쁜 제목 적용!
horizontal_spacing=0.08
)
# 4. 반복문은 실제 값(real_values)으로 돌립니다
for i, real_val in enumerate(real_values):
curr_df = clnvc_grp.filter(pl.col('CHROM_Type') == real_val)
# 내림차순 정렬해서 보기 좋게
curr_df = curr_df.sort("Total", descending=True)
fig.add_trace(
go.Bar(
x = curr_df['CLNVC'],
y = curr_df['Total'],
text = curr_df['Total'],
texttemplate = '%{y:,}',
textposition = 'outside',
# 색깔은 이미 설정한 대로!
marker_color = px.colors.sequential.algae[-(i*3+1)]
),
row = 1, col = i + 1
)
# 5. 레이아웃 정리
fig.update_layout(
height=600,
width=1250,
title_text="ClinVar Distribution by Chromosome Type",
showlegend=False,
margin=dict(t=100, l=20, r=20, b=50), # 제목 공간 확보 위해 t(top) 늘림
)
# fig.write_image('pathgenic_chr_distributuion_subplot.png')
fig.show()

그냥 SNV가 압도적이다.
chr_order = [str(i) for i in range(1, 23)] + ['X', 'Y', 'MT']
# 2. 비율 계산
clnsig_perc = clnvc_grp.with_columns(
(pl.col("Total") / pl.col("Total").sum().over("CHROM") * 100).alias("Percentage")
)
# 3. Plotly Express로 그리기
fig = px.bar(
clnsig_perc,
x = "CHROM",
y = "Percentage",
color = "CLNVC",
text = "Percentage",
title = "CLNVC ratio by Chromosome",
color_discrete_sequence = px.colors.qualitative.Vivid,
# --- 이 부분이 마법의 한 줄입니다 ---
category_orders = {"CHROM": chr_order}
# ----------------------------------
)
fig.update_traces(
texttemplate='%{text:.1f}%',
textposition='inside',
insidetextfont_family="NanumSquare_ac" # 설정하신 폰트 적용
)
fig.update_layout(
width=1250,
height=800,
barmode='stack',
xaxis=dict(title='Chromosome'),
yaxis=dict(title='Percentage (%)'),
margin=dict(t=100, l=20, r=20, b=50),
)
# fig.write_image('chr_ratio.png')
fig.show()

저게 문자로 잡혀서 순서 수기로 잡아줘야 합니다. 아무튼 각 염색체별로 제일 많은 건 SNV고 그 다음에 Deletion이다. 미토콘드리아의 경우 Deletion 다음으로 Insertion, 나머지 염색체는 Deletion 다음으로 Duplication이 제일 많다.
# 1. 염색체 정렬 순서 정의
chr_order = [str(i) for i in range(1, 23)] + ['X', 'Y', 'MT', 'M']
# 순서대로 숫자를 매칭한 딕셔너리 생성 (1:0, 2:1, ..., X:22, Y:23...)
chr_map = {val: i for i, val in enumerate(chr_order)}
# 2. 데이터 집계 및 필터링
top_gene_df = (
pathogenic_df
.group_by(['CHROM', 'GENE_SYMBOL'])
.agg(pl.count('GENE_SYMBOL').alias("Count"))
.filter(pl.col("Count") == pl.col("Count").max().over("CHROM"))
.unique(subset=["CHROM"], keep="first")
)
# 3. 정렬 로직 수정 (최신 Polars 버전 대응)
top_gene_df = (
top_gene_df
.with_columns(
pl.col("CHROM").cast(pl.Utf8)
)
.with_columns(
# replace 대신 replace_strict를 사용하고, 매핑 안 되는 값은 99로 처리합니다.
pl.col("CHROM").replace_strict(chr_map, default=99).alias("sort_idx")
)
.sort("sort_idx")
)
위에도 썼지만 X, Y가 껴서 문자라 수동으로 정렬해줘야 합니다... 아무튼 각 염색체별로 변이가 가장 많은 유전자를 갖고왔다. 판다스였으면 idxmax 쓰는건데 까비...
# 1. 표에 들어갈 데이터 정리 (위에서 정렬된 top_gene_df 활용)
header_values = ["<b>Chromosome</b>", "<b>Top Gene Symbol</b>", "<b>Variant Count</b>"]
cell_values = [
top_gene_df["CHROM"],
[f"<b>{gene}</b>" for gene in top_gene_df["GENE_SYMBOL"]], # 유전자 이름 강조
[f"{count:,}" for count in top_gene_df["Count"]] # 천 단위 콤마
]
# 2. Plotly Table 생성
fig = go.Figure(data=[go.Table(
# 컬럼 너비 비율 조절
columnwidth = [100, 150, 120],
# 헤더 스타일
header = dict(
values = header_values,
fill_color = '#2E4A3F', # Algae 계열의 짙은 색
align = 'center',
font = dict(color='white', size=16, family="NanumSquare_ac"),
height = 40
),
# 셀 스타일
cells = dict(
values = cell_values,
fill_color = ['#F5F5F5', 'white'], # 줄바꿈 배경색 효과
align = ['center', 'center', 'right'],
font = dict(color='#333', size=14, family="NanumSquare_ac"),
height = 30
)
)])
# 3. 레이아웃 설정
fig.update_layout(
title = "Top Mutated Gene by Chromosome (ClinVar Pathogenic)",
width = 800,
height = 900, # 데이터가 24줄(1~22, X, Y, M) 정도 되니 높이를 넉넉하게
margin = dict(t=80, l=20, r=20, b=20)
)
# fig.write_image('chr_gene.png')
fig.show()

그… 여러분… Plotly에서는 표를 그릴 수 있습니다. 뭐 마우스 올려도 뭐 없긴 한데 아무튼.

근데 이런것도 될 줄은 몰랐지… 이게 뭔 표임? 각 염색체별로 변이가 가장 많은 유전자들만 꼽아서 그 중에서 1, 2, 3등을 매긴 결과다. BRCA2, TTN, BRCA1 순으로 변이가 많다. 근데 저 SRY는 뭐죠? 성별 결정하는 유전자입니다. 찾아보니 고환 결정 인자란다.
# 1. 염색체 정렬 순서 정의
chr_order = [str(i) for i in range(1, 23)] + ['X', 'Y', 'MT', 'M']
# 순서대로 숫자를 매칭한 딕셔너리 생성 (1:0, 2:1, ..., X:22, Y:23...)
chr_map = {val: i for i, val in enumerate(chr_order)}
# 2. 데이터 집계 및 필터링
top_gene_df = (
pathogenic_df
.group_by(['CHROM', 'CLNVC','GENE_SYMBOL'])
.agg(pl.count('GENE_SYMBOL').alias("Count"))
.filter(pl.col("Count") == pl.col("Count").max().over("CHROM"))
.unique(subset=["CHROM"], keep="first")
)
# 3. 정렬 로직 수정 (최신 Polars 버전 대응)
top_gene_df = (
top_gene_df
.with_columns(
pl.col("CHROM").cast(pl.Utf8)
)
.with_columns(
# replace 대신 replace_strict를 사용하고, 매핑 안 되는 값은 99로 처리합니다.
pl.col("CHROM").replace_strict(chr_map, default=99).alias("sort_idx")
)
.sort("sort_idx")
)
# 1. 변이 수 기준으로 데이터 정렬 (표 상단에 TOP 3가 오게 하려면 여기서 정렬)
# 현재 top_gene_df가 염색체 순서라면, 강조 로직을 수치 기준으로 적용해야 합니다.
top_gene_df = top_gene_df.sort("Count", descending=True)
# 2. 행별 배경색 결정 함수
def get_rank_color(i):
if i == 0: return '#FFD700' # Gold (1위: BRCA2)
if i == 1: return '#E5E4E2' # Platinum/Silver (2위: TTN)
if i == 2: return '#CD7F32' # Bronze (3위: BRCA1)
return 'white' if i % 2 == 0 else '#F9F9F9' # 나머지 가독성용 줄무늬
row_colors = [get_rank_color(i) for i in range(len(top_gene_df))]
# 3. 데이터 구성
header_values = ["<b>Chromosome</b>", "<b>CLNVC</b>", "<b>Top Gene Symbol</b>", "<b>Variant Count</b>"]
cell_values = [
top_gene_df["CHROM"],top_gene_df["CLNVC"],
[f"<b>{g}</b>" if i < 3 else g for i, g in enumerate(top_gene_df["GENE_SYMBOL"])],
[f"{c:,}" for c in top_gene_df["Count"]]
]
# 4. Table 생성
fig = go.Figure(data=[go.Table(
columnwidth = [80, 150, 100],
header = dict(
values = header_values,
fill_color = '#2E4A3F', # 헤더는 짙은 Algae색 유지
align = 'center',
font = dict(color='white', size=16, family="NanumSquare_ac")
),
cells = dict(
values = cell_values,
fill_color = [row_colors] * 3,
align = ['center', 'center', 'right'],
# 글자색은 무조건 검은색 계열로 고정하여 가시성 확보
font = dict(color='#333', size=14, family="NanumSquare_ac"),
height = 35,
line_color = '#E5E5E5' # 셀 구분선 살짝 추가
)
)])
fig.update_layout(
title = "Top 3 Mutated Genes Highlighted",
width = 750,
height = 1010,
margin = dict(t=80, l=20, r=20, b=20)
)
# fig.write_image('chr_gene_clnvc.png')
fig.show()

보시면 미토콘드리아(MT-ATP6) 빼고 다 SNV죠? 저게 개인적으로는 나비효과라고 생각하는데, 염기 하나가 바뀐 것 때문에 단백질이 바뀌고, 그걸로 인해서 기능에 문제가 생기는 게 질병으로 이어지기 때문이다. 그러니까 아주 작은 염기 하나가 바뀌는 것 때문에, 염기보다 훨배 큰 사람이 영향을 받는다 이거지.

반대로 변이가 가장 적은 유전자인데... 이게 1? 왜 1이죠? 일단 이렇다 하고 단정지을 수는 없는데... 치사유전 아세요? 혈우병이 치사유전이잖아요. 여자의 경우 혈우병 유전자를 양쪽 염색체가 둘 다 갖고있으면 보통은 사산됩니다. 물론 생물학에는 100%가 없으니 그 와중에도 정말 드문 확률로 살아서 나올 가능성은 있지만...
웨스턴블록 해본 사람들은 표적 단백질 말고 컨트롤? 그런거 같이 매겨서 밴드 굵기 보셨을텐데 이걸 왜 볼까요? 그죠. 이거 발현 안되면 X되는 단백질이라 보는겁니다. 풀때기에 엽록체가 없다고 생각해보세요. 광합성을 못해서 멀리 가십니다.
아니 왜 갑자기 웨스턴 얘기가 나옴? 나도 100% 그렇다고는 못 하지만, 저 유전자들은 변이가 생기면 질병 생기는 수준이 아니라 그냥 죽거나(생존을 못하거나) 대를 못 이을 가능성이 있습니다.
'Coding > EDA' 카테고리의 다른 글
| clinVar EDA를 Polars로 해보자-Appendix. 각 변이별 일짱 (0) | 2026.04.15 |
|---|---|
| clinVar EDA를 Polars로 해보자 (0) | 2026.04.13 |
| Medical Cost Personal Datasets (0) | 2026.03.18 |
| Red Wine Quality (0) | 2026.03.13 |
| Palmer Archipelago (Antarctica) penguin data (0) | 2026.03.11 |