반응형

그 전에 clinVar 하면서 태블로 대시보드 만들었죠? 거기서 염색체별로 Top 25 만들고 그랬는데… 그거를 이제 CLNVC(변이)별로 뭐가 제일 많은지 보자는 얘기다.

 

SNV는 저번 글에 나와있었기 때문에 생략.


들어가기 전에-이게 뭔 변이임?

이 블로그에 들어오시는 분들중에는 생물정보학을 하고 있거나, 나처럼 업으로 삼지는 않았지만 거기에 관심이 있거나, 생물학 전공인 경우도 있겠지만 어때요? 여기 들어와서 clinVar라는 걸 처음 보신 분도 계시지 않습니까? 그래요, 그겁니다. EDA 따라오면서도 이게 뭐여 해서 뭔가 찾아보니 보이는 것은 꼬부랑글씨였으며…

 

물론 유전자에 문제가 생긴다고 다 질병이 되는 건 아니고, 피부 색이나 눈 색, 머리카락 색같이 사람의 형질이 달라지는 경우도 있긴 있습니다. 근데 여기 부록으로 나온건 다 패소제닉(Pathogenic)이라 뻑나면 병나는거예요.

 

1. Deletion: 일단 뭔가 나간 변이다. 나간 게 염기일수도 있고, 염색체 일부(염기 뭉텅이)일수도 있다. 그러니까 집 설계도라고 치면 벽이나 문, 창문같은게 하나 빠지거나 방 하나가 나가거나 뭐 그런거.

2. Duplication: 뭔가 1+1이 됐다. 집 설계도에 방이 두개였는데 세개가 된다거나, 창문이 하나였는데 두개가 된다거나… 이게 무조건 위험한 건 아니고 카피 넘버 배리언스(CNV)라고 해서 그 자체가 형질이 되는 경우도 있습니다. 헌팅턴 무도병도 유전자 내에 CAG가 불어나서 생기는거예요.

3. Insertion: 뭔가 껴들었다. 염기가 끼어들 수도 있고 유전자 자체가 끼어들 수도 있는데, Transposon(유전자가 이사다님)으로 인해 유전자가 끼어들어서 생기는 현상중에 유명한건 얼룩덜룩한 옥수수.

4. 1+3. 뭐가 드나드는게 인델이다. 인서션 딜리션 해서 인델.

5. Inversion: 유전자가 반대로 들어갔다. 그니까 집 설계도로 치자면 문이 밖으로 열려야되는데 안으로 열리게 들어간거다. 물론 유전자가 반대로 들어가는건 방...에 비유할수가... 있어요? 방이 위아래 거꾸로 들어갈 수가 있음?

6. Microsatellite: 짤막한 염기서열이 반복되는 돌연변이. 이것도 뭐 사람들한테 위험하다… 이런 건 아니고… 맞죠? 전공수업때는 못봤던거임…

7. Variation: 찾아봤더니 사전적인 정의가 나온다…

Deletion

Duplication

Insertion

Indel

Y랑 미토콘드리아는 걍 없나본데...?

Inversion

Microsatellite

Variation

 

반응형

'Coding > EDA' 카테고리의 다른 글

clinVar EDA를 Polars로 해보자-Pathogenic EDA  (0) 2026.04.14
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
반응형

지난 이야기: 아 염색체별로 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% 그렇다고는 못 하지만, 저 유전자들은 변이가 생기면 질병 생기는 수준이 아니라 그냥 죽거나(생존을 못하거나) 대를 못 이을 가능성이 있습니다.

반응형
반응형

전에 깔짝깔짝 판다스랑 비교했던 폴라스로… EDA가 될지 해봤다. 그래서 전에 했던거랑 내용은 같은데, 비교하는 툴이 달라지는겁니다. 이거 아마 포폴에도 폴라스 플롯틀리로 올라갈듯함. 근데 새로 나온건 알겠어, 이걸 써봐야 해? 네카라쿠배의 배에서 쓴답니다.

 

전처리는 이전 과정이랑 비슷하니까 그룹바이랑 필터 위주로 ㄱㄱ합시다.


clinvar_df = pl.read_csv('data/clinvar_20260404.csv', infer_schema_length=0)

얘는 판다스에서 붙는 메모리 관련 옵션이 아예 안 붙는다. 근데 뭔가 붙어있지 않냐고? 걍 열면 Original error: invalid primitive value found during CSV parsing 에러 뜨니까 걍 다 읽고 판별하셈 한 겁니다. 그렇게 해도 1초 좀 넘게 걸려요.


CLNSIG으로 묶기

이게 일단 돌리는 방식은 예전이랑 동일한데, 데이터가 달라져서(4월 4일자껄로 진행) 결과가 달라질 수 있다. 4월 4일자에 든 게 4,403,603개고 전에 했던건 좀 적게 들어있었음... 그래서 총계 이런거에서는 차이가 날 수밖에 없음.

 

clinvar_df.group_by('CLNSIG_Group').agg(
    pl.col('CLNSIG').count().alias("Total")
)

놀라운 사실을 하나 알려주지면, Polars에서는 단순히 그룹바이가 되는 걸 떠나서 .agg로 집계 매긴 다음 그 집계 칼럼에 별칭을 붙일 수 있다. 판다스도 되긴 되겠지만 쟤는 한번에 저게 된다. 두번째줄 pl.col('CLNSIG').count().alias("Total")가 무슨 의미냐면 CLNSIG 칼럼으로 묶고 셀 건데 그 결과물 칼럼 이름을 Total로 해달라는 의미. SQL 해보셨으면 감이 좀 왔을 것이다. 

 

당연한 얘기지만, alias 없어도 저 코드는 작동을 한다. 근데 어지간하면 있는 편이 좋다.

alias 안 주면 Total이 아니라 CLNSIG으로 나오는데 이게 뭔줄 앎? 그럼 이걸로 이제 플롯틀리 그래프를 그리기 전에 정렬을 좀 해야되는데…

 

clinvar_df.group_by('CLNSIG_Group').agg(
    pl.col('CLNSIG').count().alias("Total")
).sort("Total", descending=True) # 정렬을~ 돌려다아아아오~

걍 뒤에 소트 붙이시면 알아서 정렬 됩니다. 모든 정렬은 오름차순이 기본이니 내림차순으로 정렬해주면 된다. 이걸 이제 그대로 플롯틀리에 떤지면 안되고… 변수명 할당해서 줘야됨.

 

fig = go.Figure()

fig.add_trace(
    go.Bar(x = clnsig_grp['CLNSIG_Group'], y = clnsig_grp['Total'], marker_color = px.colors.sequential.Cividis,text = clnsig_grp['Total'])
)

fig.update_layout(
    width = 1200, height = 800,
    xaxis=dict(title='ClinVar Significance Group'),
    yaxis=dict(title='Count'),
    title = "ClinVar Group Distribution"
)

# fig.write_image('group_distributuion.png')
fig.show()

여러분들은 여기서 피똥 쌀 것이다. 일단 나는 여기서 피똥 쌌음... 아니 전역으로 컬러테이블 박아놨는데 마커는 또 일일이 주래요 이게 말이야 당나귀야 세상에...

 

그 결과물이 이거임. 다행히도 폰트 전역으로 설정해둔 건 듣는다. 어 근데 저기 저장 코드는 왜 주석처리 했어요? Plotly는 그래프 복사가 안 되고 저장한 다음에 올려야 하는데, 문제는 그 저장하는걸 할 때 시간이 증말 완전 개같이 오래 걸립니다. 그래서 그래프를 보여줄때는 저 코드에 주석을 씌워놓고 그래프_최종_진짜최종_저장.png 느낌으로 저장할때만 주석을 해제하는 것이다. 참고로 저거 그냥 저장되는 거 아니고 kaleido 까셔야 합니다.

 

# 트리맵은 Plotly Express(px)가 훨씬 직관적입니다.
fig = px.treemap(
    clnsig_grp,
    path = ['CLNSIG_Group'], # 계층 구조 (여기선 그룹 하나)
    values = 'Total',
    color = 'Total',         # 숫자에 따라 색상 농도 조절
    color_continuous_scale = 'algae',
    title = "ClinVar Significance Hierarchy"
)

# 전역 설정된 폰트 적용 확인
fig.update_layout(width=1200, height=800)
# fig.write_image('group_distributuion_heatmap.png')
fig.show()

이 트리맵에는 문제가 하나 있다. 저기 막대그래프를 보시면 Risk/Other가 다른 값들에 비해 비중이 좀 적죠? 예. 블록이 안보입니다. 착한 사람만 보이는 블록임.

 

fig = go.Figure()

fig.add_trace(go.Pie(
    labels = clnsig_grp['CLNSIG_Group'],
    values = clnsig_grp['Total'],
    textinfo = 'label+percent', # 이름과 퍼센트 동시에 표시
    insidetextorientation = 'radial',
    marker = dict(colors=px.colors.sequential.algae) # 전역 설정 색감 유지
))

fig.update_layout(
    title_text = "ClinVar Significance Proportion",
    width = 1200,
    height = 800,
    margin = dict(t=50, l=10, r=10, b=10)
)

# fig.write_image('group_distributuion_pie.png')
fig.show()

여기서도 안보이죠? 이정도면 거의 주서터널현미경 빌려서 봐야됨… ㅋㅋ

 

CLNSIG-Chromosome

clnsig_grp = clinvar_df.group_by(['CLNSIG_Group', 'CHROM_Type']).agg(
    pl.col('CLNSIG').count().alias("Total")
).sort('CLNSIG_Group')

CLNSIG-염색체 그룹으로 묶어보았다. 언노운은 몇 번 염색체인지 불명인 것 같음.

 

max_total = clnsig_grp['Total'].max()
sizeref = 2. * max_total / (100**2) # 100은 최대 버블 반지름(픽셀)

fig = go.Figure()

# 2. 그룹별로 순회하며 트레이스 추가
# 이렇게 하면 전역 설정된 colorway 순서대로 색이 입혀집니다!
for group_name in clnsig_grp['CLNSIG_Group'].unique():
    # 해당 그룹 데이터만 필터링
    curr_df = clnsig_grp.filter(pl.col('CLNSIG_Group') == group_name)

    fig.add_trace(go.Scatter(
        x = curr_df['CLNSIG_Group'],
        y = curr_df['CHROM_Type'],
        name = group_name, # 범례에 표시될 이름
        mode = 'markers+text',
        marker = dict(
            size = curr_df['Total'],
            sizemode = 'area',
            sizeref = sizeref,
            sizemin = 10,
            # 여기서 color를 지정하지 않으면 전역 설정(colorway)을 따라갑니다!
        ),
        text = curr_df['Total'],
        texttemplate = "%{x}<br>%{text:,d}",
        textposition = "top right",
        textfont = dict(family="NanumSquare_ac", size=12)
    ))

# 3. 레이아웃 설정 (컬러바는 어차피 안 나오지만 확인사살)
fig.update_layout(
    width = 1200,
    height = 800,
    title = "ClinVar Significance by Group (Global Palette Applied)",
    showlegend = True, # 그룹별로 색이 다르니 범례를 보여주는 게 좋습니다
)

# fig.write_image('cln-chr bubble.png')
fig.show()

스읍… 이거 색깔을 x축별로 바꾼건데… algae로 했더니 표가 너무 안남..

 

오… 일단 언노운 퇴근시키자.

 

굿. 저 버블이랑 라벨 간격은 우리가 조절을 못해요...

 

염색체별 CLNSIG 비율

fig = go.Figure()

fig.add_trace(
    go.Bar(x = clnsig_grp['CHROM_Type'], y = clnsig_grp['Total'], marker_color = px.colors.sequential.algae, text = clnsig_grp['CLNSIG_Group'])
)

fig.update_layout(
    width = 1200, height = 800,
    xaxis=dict(title='Chromosome Type'),
    yaxis=dict(title='Count'),
    title = "ClinVar Group Distribution",
    margin = dict(t=50, l=10, r=10, b=10)
)

# fig.write_image('group_distributuion.png')
fig.show()

이거 일단 이대로도 볼 수는 있거든요? 근데 문제가 하나 있음.

 

악 내눈!

 

일단 왜 이런 사태가 일어났는지를 먼저 봐야 한다. clinVar 데이터에 들어있는 염색체가 언노운 빼고 총 25종인데 그 25종이 각개가 아니라 1~22번까지가 상염색체(남녀 다 똑같이 있는거), X랑 Y가 성염색체(남녀 분포 달라요), 그리고 미토콘드리아가 있어요. 이게 다 길이가 같다고 쳐도 쪽수때문에 일단 상염색체 물량이 압도적인데 염색체 그림 찾아보면 어때요? 1번 봐봐요. 크기가 벌써부터 이놈은 키가 커서 1번인가 하게 길어요…

 

그러니까 상염색체에 비하면 다른 염색체들은 거의 뭐 종합운동장에 축구공 하나씩 굴러다니는 정도로 미미한 수준이라 저렇게 나오는거다. 저걸 파훼할 방법이 있냐고?

 

# 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 = clnsig_grp.filter(pl.col('CHROM_Type') == real_val)

    # 내림차순 정렬해서 보기 좋게
    curr_df = curr_df.sort("Total", descending=True)

    fig.add_trace(
        go.Bar(
            x = curr_df['CLNSIG_Group'],
            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('group_distributuion_subplot.png')
fig.show()

서브플롯을 쓰던가 

 

# 1. 비율(Percentage) 계산 (Polars 활용)
clnsig_perc = clnsig_grp.with_columns(
    (pl.col("Total") / pl.col("Total").sum().over("CHROM_Type") * 100).alias("Percentage")
)

# 2. Plotly Express로 그리기 (비율 차트는 PX가 압도적으로 편합니다)
fig = px.bar(
    clnsig_perc,
    x = "CHROM_Type",
    y = "Percentage",
    color = "CLNSIG_Group", # 그룹별 색상
    text = "Percentage",
    title = "ClinVar Significance Proportion (100% Stacked)",
    color_discrete_sequence = px.colors.qualitative.Vivid # 아까 쓰신 Prism!
)

fig.update_traces(texttemplate='%{text:.1f}%', textposition='inside')
fig.update_layout(width=1200, height=800, barmode='stack')

# fig.write_image('group_distributuion_stack.png')
fig.show()

누적... 그리셔도 되는데 문제가 뭐다? Risk/Other가 안보여요...

 

이제 다음편에서 Pathogenic한 변이만 골라서 또 해볼거다. 이거 한번에 하면 님들 데이터 요금 급나나가요.

반응형
반응형

보통 판다스 데이터프레임 불러와서 지지고 볶고 뭐 해요? 그래프 그리죠. 표로 정리해서 보여주는것보다 그래프 딱 만들어서 도표 딱 보여주면 기깔나쟎아요? 그겁니다. 그리고 우리가 제일 많이 쓰는 맷플롭이나 씨본(+Plotly)에서도 폴라스를 받아줄지 궁금해서 해봤습니다.

 

이번에 써 볼 데이터프레임은 파일 불러온거 하나(켐플) 있고, 직접 만든거 하나 있습니다.


import polars as pl
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px # 프로젝트 이런거 아니니까 걍 얘 부를게여

위에서부터 순서대로 폴라스, Matplotlib, 씨본, Plotly임다. 불러오십쇼. pyarrow는 불러올 필요 없고 설치만 하면 됨.

 

# 피보나치 수열
dict = {
    'A':[1, 1, 2, 3, 5, 8, 13, 21, 34, 55],
    'B':[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}

# 을 데이터프레임으로 
df = pl.DataFrame(dict)

그리고 직접 만든 데이터는 이겁니다. 피보나치 수열임.


Matplotlib

둘다 잘 됨. 진짜로 그게 답니다. Null이 있으면 걍 이게 뭐여 패스 하는듯.

 

위가 피보나치, 아래가 산점도다. ...저기 0이 널 아니예요? 나도 그런 줄 알고 확인해봤는데 그냥 수소결합을 안 하는 놈임.

 

Seaborn

근데 왜 씨본은 색이 다르냐면 쟤는 Hue 옵션을 줬음. 쟤만 줬어요.

 

sns.scatterplot(load_df.to_pandas(), x = 'HBA', y = 'HBD')
plt.show()

산점도는 그리다가 결측값때문에 에러가 났는데, 판다스는 NaN(날짜는 NaT)이지만 폴라스는 null이잖아요? 이걸 결측값 처리 안 하고 걍 주면 씨본이 이게 뭐시여 하고 오류를 토하니까 to_pandas()를 준 겁니다. 근데 저게 pyarrow가 설치되어있어야 가능하니까 또 설치한건데… 여러분들이 EDA를 할 거라면 안 쓸 칼럼은 날릴거고, 그게 아니어도 결측값을 어떻게든 채울거잖아요? 결측값 땜질만 적절하게 하면 오류는 볼 일이 없습니다.

 

Plotly

쟤는 왜 캡쳐했나요? 플롯틀리는 그래프 복사가 안됩니다. 쟤 사이즈 왜저래요? 사이즈가 불만이면 그릴때 조절해야 합니다. 조절 안하면 저렇게 넙대대한 놈 나옵니다.

 

아무튼 둘 다 오류 없이 잘 나온 건 맞다.

반응형

'Coding > Python' 카테고리의 다른 글

Polars를 써보자  (0) 2026.04.09
MSA에 군집분석을 끼얹어보세요!  (0) 2026.02.20
M1V1 = M2V2  (0) 2026.02.15
코로나바이러스 MSA  (0) 2026.02.05
라이노바이러스 유전자로 MSA를 해보았다  (0) 2026.01.27
반응형

엥? 폴라스? 그게 뭐예요오?

 

Polars is an open-source library for data manipulation, known for being one of the fastest data processing solutions on a single machine. It features a well-structured, typed API that is both expressive and easy to use.

 

뭐라는겨 싶겠지만 폴라스는 판다스 비슷한 일을 한다. 데이터프레임을 만들거나 불러오거나 하는 모든 일들이 가능한데 일단 읽는 속도가 판다스보다 20배 빨랐음. 뭐 메모리 어쩌고 하던데 나는 컴퓨터 아키텍쳐까지는 잘 모르니까 패스하고... 판다스랑 좀 다른 부분이 있다는 건 유념하십쇼.


import polars as pl

일단 판다스처럼 폴라스도 깔고 불러야 쓸 수 있다. 넘파이...는... 불러야되나?

 

파~일을 열어다~오~

df_csv = pl.read_csv("data/Fluoxetine.csv", separator=";", null_values=["", '""', 'None']) # ChEMBL은 구분자가 세미콜론임다 
df_csv2 = pd.read_csv("data/Fluoxetine.csv", sep=";")

여기서 문제가 하나 있음. 저기서 어떤게 폴라스고 어떤게 판다스게요? 위에껍니다. 세부적인 옵션 빼고 걍 불러오는건 폴라스나 판다스나 비슷해서 pl pd로 구별해야되는데 이게 하다보면 그게 잘 안됩니다. 둘 중 하나만 쓰거나 둘 다 써야되는 상황이면 변수명으로 구별하던가 주석을 다십시오.

 

separator=";"는 csv 구분자 옵션인 거 알겠는데 저 뒤에놈은 뭔가요? 저게 켐블 불러온건데, 구분자가 ;이고 없는건 걍 없거든요? 그러면 판다스는 아 없군 결측값! 하는데 폴라스는 ""로 때워버립니다. 이게 그리고 NaN이랑 null이랑 달라요 폴라스는. 폴라스에서는 결측값이 널인데 이건 SQL의 널 비슷하게 흘러갑니다. ""는 어쨌든 뭐가 있는거니까 결측값이 아니래요. 그래서 부를때 자 이게 결측값이야~ 도 같이 해 준 거다.

 

참고로 같은 파일 불러오는데 폴라스는 2밀리초, 판다스는 21밀리초 걸림.

 

내 하드에 저-장

쓰는것도 코드 자체는 차이가 없다. to_csv 하면 되는'데' 저장한 결과물에서 차이가 난다. 판다스 써보신 분들 to_csv로 저장한거 다시 열었더니 맨 왼쪽 칼럼에 Unnamed:0 생긴 적 있으시죠? 그런거 한 두세번 돌리면 언네임드만 주루룩 생기고 그래서 열고 드롭한 적 있으시죠? 판다스에서 csv로 저장할때는 index=False를 줘야 그게 안 생긴다.

 

그럼 폴라스는요? 폴라스는 그 옵션 안 줘도 인덱스가 따로 저장이 안 된다. 대신 저장하는데 드는 소요시간은 비슷했음.

 

결측값

위에도 썼지만 결측값이 폴라스는 널이다. 그래서 .isna() 비슷한 함수가 .is_null()이고 .isna().sum() 비슷한건 .null_count()인데... 이거 가로로 나옵니다... 그리고 폴라스는 표 출력하면 위에 shape가 같이 나옴.

 

쿼리 되나요?

# 1. 컬럼명 변경 (성공적)
df_csv = df_csv.rename({"ChEMBL ID": "ChEMBL_ID"})

# 2. 컨텍스트 및 테이블 등록
ctx = pl.SQLContext()
ctx.register("chembl_table", df_csv)

# 3. 쿼리 실행 (쌍따옴표 제거!)
result = ctx.execute("""
    SELECT * FROM chembl_table 
    WHERE ChEMBL_ID = 'CHEMBL153036'
""").collect()

result

폴라스에서는 당신이 SQL 쌉고인물이라면 파이썬으로 불러와놓고 SQL 쿼리를 쓸 수 있다. 실화임. SELECT * FROM chembl_table WHERE ChEMBL_ID = 'CHEMBL153036' <<이게 SQL 쿼리예요.

 

df_csv.filter(pl.col("ChEMBL ID") == "CHEMBL153036")

아니 그럼 SQL 알못들은 어쩌라고요! 아 필터 쓰십쇼.

 

데이터의 정보를 확인하는 방법

요즘 EDA할때 생략하는 그거 맞다. 폴라스... 하게되면 이거는 정보 확인하는거 한번 올려드림. 판다스랑 기능은 똑같은데 토해내는게 달라요.

 

df_csv.schema

.info() 비슷한 기능을 하는게 .schema인데 인포처럼 칼럼 몇갠데 몇개 비었고 이런건 안나오고 뭔 타입인지만 나온다. 인포 비슷한거 필요하시면 .glimpse() 쓰십쇼. 아니면 널카운트? 

 

아니면 .describe()에서 칼럼별 통계값 보고 알 수 있다. 예? 뭔 소리임? 폴라스에서는 널이 결측값이라고 했잖아요? 근데 SQL에서 연산에 널끼면 다 널됩니다. 왜냐고요? 널이 응 아니 몰라에서 몰라거든. 설문조사로 치자면 예 아니오 무응답에서 무응답이다. 슈뢰딩거의 고양이는 박스 까보면 얘가 갔는지 있는지는 알 수 있지만 널은 박스 밀봉된 슈뢰딩거의 고양이라 진짜 모름.

 

이 글에서는 그냥 데이터 열고 정보 확인하는것만 비교해봤는데, 다음편에서는 시각화 어떻게 되나 해 볼 예정이다. matplotlib나 seaborn에서는 폴라스를 못 쓴다는 얘기가 있음.

반응형
반응형

이거어어어어는... 돌리는건 하나 돌렸습니다. 그럼 왜함? 그 옵튜나인가 뭔가 하는 거 써보려고 했음.


Optuna

파이썬 라이브러리인데, 하이퍼파라미터 튜닝할때 쓴다. 모델 학습 전 사용자가 직접 설정하는 외부 구성 변수를 하이퍼파라미터라고 하는데, 이걸 사람이 수동으로 일일이 조정해가면서 어느 조건에서 내 모델을 뽕을 잘 뽑을지를 고민...하면서 일일이 하다 보면 할 것도 많고 파라미터 하나하나 일일이 손대가면서 찾기도 힘들잖아요? 그 노가다를 알아서 해주는게 옵튜나입니다. 

 

모델 바이 모델이라 함수에 집어넣는 게 다른데, 아무튼 이거 이거 이거 해줘 하면 지 알아서 음 이렇군 하면서 오케이 가릿 이걸로 진행시켜 한다. 모델에 따라서는 시간이 좀 걸리기도 합니다.


안하면 섭한 전처리

자녀 수 범주화

# 자녀 수는 모르겠고 유무로 범주화할거임. ㅇㅋ? ㅇㅇㅋ.
cost['have_child'] = cost['children'].apply(lambda x: 0 if x == 0 else 1) # 자녀수가 0이면 0, 0보다 크면 1
# 유자녀가 1입니다

심플하게 유자녀 무자녀로 범주화했다.

 

일부 칼럼 퇴근시키기 

cost.drop('region', axis = 1, inplace = True)
cost.drop('children', axis = 1, inplace = True)

자녀 칼럼: 범주화했으니까 퇴근해도 됨/지역 칼럼: 학습에 도움 안 될 것 같아서 퇴근

 

스케일뤄어어어어

scaler = StandardScaler()

cost[['age','bmi']] = scaler.fit_transform(cost[['age','bmi']])

이정도면 다들 저거 뭔지 아시죠?

 

매핑

# 흡연여부
cost['smoker'] = cost['smoker'].map({'yes': 1, 'no': 0})
cost['sex'] = cost['sex'].map({'male': 1, 'female': 0})

아니 성별은 원핫인코더 쓰려고 했더니 에러토하데… ㅡㅡ


옵튜나 써보기

# 1. 독립변수(X)와 종속변수(y) 분리
X = cost.drop('charges', axis=1)
y = np.log1p(cost['charges']) # 아까 말씀드린 로그 변환!

# 2. 데이터 분할 (보통 8:2나 7.5:2.5로 나눕니다)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

print(f"학습 데이터 개수: {len(X_train)}")
print(f"테스트 데이터 개수: {len(X_test)}")

근데 우리 이거 쓰기 전에 학습용 테스트용 나눠야되는건 아시죠?

 

def objective(trial):
    param = {
        'n_estimators': trial.suggest_int('n_estimators', 500, 2000),
        'max_depth': trial.suggest_int('max_depth', 3, 9),
        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.1),
        'subsample': trial.suggest_float('subsample', 0.6, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
        'random_state': 42
    }

    model = XGBRegressor(**param)
    model.fit(X_train, y_train)

    preds = model.predict(X_test)
    rmse = np.sqrt(mean_squared_error(y_test, preds))

    return rmse

# 최적화 시작
study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=50) # 50번 정도만 돌려봐도 감이 옵니다.

print(f"최고의 파라미터: {study.best_params}")

이렇게 범위를 정해주면... 그 범위 뭐가 뭔지 모르겠으면 에미나이나 지피티, 클로드한테 나 이 모델 돌릴건데 옵튜나 짜줘 하시면 됩니다... 아무튼. 저렇게 주고 맞춰주십쇼 하면 옵튜나가 아 ㅇㅋㅇㅋ 해봅시다! 하면서 뭔가 돌아가요.

 

최고의 파라미터: {'n_estimators': 815, 'max_depth': 3, 'learning_rate': 0.017046309566345508, 'subsample': 0.9510680208670862, 'colsample_bytree': 0.8874051559739393}

그리고 다 돌아가면 이렇게 돌리면 이렇게 나온다! 하고 알려줍니다. 회귀 돌릴때 옵튜나 쓰면 최상의 파라미터랑 그걸로 돌렸을때 R^2도 같이 나옴.

 

학습

model = XGBRegressor(**study.best_params)
model.fit(X_train, y_train)

하아니 그럼 저 파라미터를 다 복붙해야 하나요? 아니, 그런 귀찮은 짓을 할 필요가 없어요. 우리는 저 파라미터를 변수에 할당해뒀으니까 그거 앞에 애스터리스크 두개 붙여서 모델에 넣으십쇼.

 

# 1. 예측 수행 (로그 상태)
y_pred_log = model.predict(X_test)

# 2. 역변환 (로그 -> 원래 달러 단위)
y_pred = np.expm1(y_pred_log)
y_actual = np.expm1(y_test)

# 3. 성능 지표 확인
print(f"R² Score (결정계수): {r2_score(y_actual, y_pred):.4f}")
print(f"MAE (평균 절대 오차): ${mean_absolute_error(y_actual, y_pred):.2f}")
print(f"MSE (평균 제곱 오차): ${mean_squared_error(y_actual, y_pred):.2f}")
print(f"RMSE (제곱근 평균 제곱 오차): ${np.sqrt(mean_squared_error(y_actual, y_pred)):.2f}")
R² Score (결정계수): 0.8768
MAE (평균 절대 오차): $1999.14
MSE (평균 제곱 오차): $19125564.44
RMSE (제곱근 평균 제곱 오차): $4373.28

?? 근데 내 예상보다 더 잘나왔어 이거...

반응형
반응형

오늘은 회귀입니다. …회기는 그 경희대 있는 지하철역이예요 선생님들.


주성분분석

머고 오늘은 ML 안함? 아니 할건데… 저게 회귀분석 할거라고 했죠? 저 데이터 칼럼이 12개인데 하나빼고 다 독립변수니까 지금 독립변수가 11개거든요. 그거 그대로 때려박으면 다중공선성 터질수도 있으니까 압축할 수 있는건 압축하고 가자 이겁니다. 11개 언제 넣었다 뺐다 할거임?

 

wine_x = wine.copy()
X = wine_x.drop('quality', axis=1)
y = wine['quality']

# 1. 스케일링 (PCA 전 필수!)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 2. PCA 객체 생성 (일단 모든 성분을 다 뽑아봅니다)
pca = PCA()
X_pca = pca.fit_transform(X_scaled)

그나마 데이터가 다 수치형이라 FAMD 안 가고 주성분분석 했음.

 

# 전체 성분(11개)에 대해 PCA 수행
pca_full = PCA().fit(X_scaled)

# 개별 분산 설명력
individual_var = pca_full.explained_variance_ratio_
# 누적 분산 설명력
cum_var = np.cumsum(individual_var)

# 결과 출력
for i, (ind, cum) in enumerate(zip(individual_var, cum_var)):
    print(f"PC{i+1}: 개별 {ind:.2f} / 누적 {cum:.2f}")
    if cum >= 0.85 and i > 0 and cum_var[i-1] < 0.85:
        print(f"--- 여기까지 딱 끊으면 정보의 {cum*100:.1f}%가 보존됩니다! ---")
PC1: 개별 0.28 / 누적 0.28
PC2: 개별 0.18 / 누적 0.46
PC3: 개별 0.14 / 누적 0.60
PC4: 개별 0.11 / 누적 0.71
PC5: 개별 0.09 / 누적 0.80
PC6: 개별 0.06 / 누적 0.86
--- 여기까지 딱 끊으면 정보의 85.5%가 보존됩니다! ---
PC7: 개별 0.05 / 누적 0.91
PC8: 개별 0.04 / 누적 0.95
PC9: 개별 0.03 / 누적 0.98
PC10: 개별 0.02 / 누적 0.99
PC11: 개별 0.01 / 누적 1.00

제 6 주성분까지 채용하면 85% 이상의 설명력을 갖는다. 그래서 그 주성분들이 뭔데?

 

for i in range(n_comp):

    loading_scores = pd.Series(
        pca_full.components_[i],
        index=X.columns
    )

    sorted_loadings = loading_scores.abs().sort_values(ascending=False)

    print(f"\nPC{i+1} 핵심 변수 TOP3")
    print(sorted_loadings.head(3))
PC1 핵심 변수 TOP3
fixed acidity    0.489314
citric acid      0.463632
pH               0.438520
dtype: float64

PC2 핵심 변수 TOP3
total sulfur dioxide    0.569487
free sulfur dioxide     0.513567
alcohol                 0.386181
dtype: float64

PC3 핵심 변수 TOP3
alcohol                0.471673
volatile acidity       0.449963
free sulfur dioxide    0.428793
dtype: float64

PC4 핵심 변수 TOP3
chlorides         0.666195
sulphates         0.550872
residual sugar    0.372793
dtype: float64

PC5 핵심 변수 TOP3
residual sugar    0.732144
alcohol           0.350681
pH                0.267530
dtype: float64

PC6 핵심 변수 TOP3
pH                  0.522116
volatile acidity    0.411449
density             0.391152
dtype: float64

이 에미나이 변수 압축한다고 했는데 산점도 볼때부터 알아봤어야 했는데…

 

1. 제 1주성분: fixed acidity, citric acid, pH
2. 제 2주성분: total sulfur dioxide, free sulfur dioxide, alcohol
3. 제 3주성분: alcohol, volatile acidity, free sulfur dioxide
4. 제 4주성분: chlorides, sulphates, residual sugar
5. 제 5주성분: residual sugar, alcohol, pH
6. 제 6주성분: pH, volatile acidity, density

 

이렇게 나왔으면 중복 쳐내면 됩니다.

 

{'free sulfur dioxide', 'fixed acidity', 'sulphates', 'pH', 'total sulfur dioxide', 'residual sugar', 'density', 'chlorides', 'alcohol', 'volatile acidity', 'citric acid'}

저거 리스트에 다 때려박고 set으로 바꾸면 중복 다 쳐냄.

 

회귀분서억

X = X_scaled # 저기 주성분 돌리기 전에 거쳤어요 스케일러

model = LinearRegression()
model.fit(X, y)

pred = model.predict(X)
print("MAE:", mean_absolute_error(y, pred))
print("MSE:", mean_squared_error(y, pred))
rmse = np.sqrt(mean_squared_error(y, pred))
print("RMSE:", rmse)
print("R²:", r2_score(y, pred))
MAE: 0.5004899635644883
MSE: 0.416767167221408
RMSE: 0.6455750670692045
R²: 0.3605517030386882

거 설명력이 너무 약한 거 아니오?

 

잔차분석 했더니 고질라 왔다간거 실화냐?

 

VIF(다중공선성)

이거 왜 보냐면 독립변수끼리 상관이 있나를 보는겁니다. 지들끼리 상관이 있으면 모델 시망됨.

 

wine_x = wine_x.drop('quality', axis=1)

# X는 독립변수 데이터프레임 (스케일링 안 해도 되지만 보통 해도 상관없음)
X_vif = pd.DataFrame()
X_vif["variable"] = wine_x.columns
X_vif["VIF"] = [variance_inflation_factor(wine_x.values, i) for i in range(wine_x.shape[1])]

print(X_vif.sort_values(by="VIF", ascending=False))
                variable          VIF
7                density  1479.287209
8                     pH  1070.967685
10               alcohol   124.394866
0          fixed acidity    74.452265
9              sulphates    21.590621
1       volatile acidity    17.060026
2            citric acid     9.183495
4              chlorides     6.554877
6   total sulfur dioxide     6.519699
5    free sulfur dioxide     6.442682
3         residual sugar     4.662992

망했는데…? 들어내봐야겠지 이거…?

 

부록-알콜과 퀄리티

wine.corr()['quality'].sort_values(ascending=False)
quality                 1.000000
alcohol                 0.476166
sulphates               0.251397
citric acid             0.226373
fixed acidity           0.124052
residual sugar          0.013732
free sulfur dioxide    -0.050656
pH                     -0.057731
chlorides              -0.128907
density                -0.174919
total sulfur dioxide   -0.185100
volatile acidity       -0.390558
Name: quality, dtype: float64

알콜이 상관계수가 제일 높은데?

 

plt.figure()

scatter = plt.scatter(
    wine['alcohol'],
    wine['quality'],
    c=wine['quality'],
    cmap='viridis',
    alpha=0.6
)

plt.xlabel('Alcohol')
plt.ylabel('Quality')
plt.title('Alcohol vs Wine Quality')

plt.colorbar(scatter, label='Quality')
plt.show()

 

이냥반들아 알콜 많이 들어가봐야 소독용 에탄올이라고... 내가 그래서 고량주 안마심. 실험실에서 맡던 소독용 에탄올 냄새 나서...

 

부록 2-XGBoost가 여기서 왜 나와요

우리 방금까지 회귀했는데 쟈는 또 뭐임? 어제 그 분류했던 친구입니다. 이 데이터셋으로 회귀도 하고 분류도 한다고 함.

 

# 일단 쨈
wine_df = wine.copy()
X = wine_df.drop("quality", axis=1)
y = wine_df["quality"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

scaler = StandardScaler()

X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
model = XGBRegressor(
    n_estimators=300,
    max_depth=4,
    learning_rate=0.05,
    random_state=42
)

model.fit(X_train, y_train)

pred = model.predict(X_test)

print("R2:", r2_score(y_test, pred)) # R2: 0.4512701630592346

어째 회귀보다 분류가 설명력이 더 좋은 것 같다.

 

SHAP

얘는 또 뭥미? 이건 그러니까 XGBoost와 대화의 시간을 가지면서 왜 그렇게 분류한건지 물어보는 친구다. 오은영박사님 어… 아니 생각하는 의자까지는 아니고…

 

# 1. 모델의 predict 함수를 직접 전달
# 2. masker를 사용하여 데이터의 통계적 분포를 SHAP에게 알려줍니다.
masker = shap.maskers.Independent(data=X_test)
explainer = shap.Explainer(xgb_model.predict, masker)

# 3. SHAP 값 계산 (Permutation 방식은 속도는 좀 걸리지만 매우 정확합니다)
shap_values = explainer(X_test)

# 4. 시각화
shap.summary_plot(shap_values, X_test)

그니까 이제 XGBoost한테 저 샤프가 오은영박사님에 빙의해서 왜 이렇게 분류했냐고 물어봤을 거 아닙니까? 그러니까 모델이 인제 얘를 이렇게 분류한 이유를 얘기해주는겁니다. 얘는 산도가 너무 높아서 이렇게 했고 얘는 밀도가 이래서 이렇게 분류했어요, 이렇게. 그게 저 그래프임다. 

반응형
반응형

예… 그… 펭귄 데이터입니다. 그건 아는데 이걸 왜 꺼냈냐… 분류 할거라서 꺼냈습니다. 예. 아니 진짜 할거임.


전처리 하기 전에...

그냥 EDA였으면 범주화하고 결측값 확인하고 채우거나 날리거나 했을텐데, 이번에는 그렇게 하고 땡 하면 안된다. 왜냐… 무작정 아무 칼럼이나 학습하는데 썼다간 모델 성능이 떨어지거든요. 그리고 범주형 칼럼중에 학습에 쓸 칼럼은 인코딩도 해 줘야 한다.

 

학습에 쓸 수 없는 칼럼 날리기

내 일일이 올리기 귀찮아서 올리지는 않는다만... 분석하기 전에 항상 .head()랑 .column 써서 뭐 있는지 보고 가죠? shape는 잘 안씀... 아무튼. 거기서 칼럼들을 확인해보고 우선 날릴 것부터 정할거다.

# Copy
penguin_drop = penguin.copy()

# 하고 날려날려 칼럼
penguin_drop.drop(['studyName','Sample Number','Region','Individual ID','Clutch Completion', 'Comments', 'Date Egg', 'Stage'], axis=1, inplace=True)
penguin_drop.head()

이걸 날리는 이유는 되게 간단하다. 학습에 도움이 안 돼서.

 

결측값 채우기

sns.kdeplot(penguin_drop['Culmen Length (mm)'])

롸? 갑자기 쟤가 왜 나옴? 임퓨터가 평균, 중앙값, 최빈값으로 채워주는건데 문제가 하나 있습니다. 그게 값 분포에 따라 적절한 걸 골라야지 덮어놓고 히히 평균해야징 하다간 모델 성능이 똥멍청이 1이 됩니다. 그래서 때우기 전에 분포를 본 건데, 이게 보니까… 저거 그 어린왕자 그 코끼리 그거 아님? 그럼 어떻게 합니까?

 

num_cols = ['Culmen Length (mm)', 'Culmen Depth (mm)', 'Flipper Length (mm)', 'Body Mass (g)'] # 일단 떄울 칼럼

imputer = KNNImputer(n_neighbors=5) # 얘는 그 이웃 참고해서 때워주는 친구입니다
penguin_drop[num_cols] = imputer.fit_transform(penguin_drop[num_cols]) # 때-움

임퓨터중에 KNN Imputer라는 게 있는데, 얘는 결측값을 이웃한 그룹들을 참고해서 채워주는 친구다. 그렇게 해서 몸무게까지 네개 채워주면 일단 끝… 왜 저걸 일괄로 채움? 이유는 간단하다. 저 칼럼에 결측값이 두개씩 있었는데 그 두개가 같은 펭귄에서 빠져있었거든…

 

# . 대치합니닷 
penguin_drop['Sex'] = penguin_drop['Sex'].replace('.', np.nan)

penguin_drop['Sex'].unique()

성별은 범주형이라 최빈값으로 채워야 하는데, 그거 말고도 다른 문제가 있다. 저 점 뭐야 점. 저것도 결측값으로 바꾸고 최빈값으로 때울거다.

 

imputer = SimpleImputer(strategy='most_frequent')
penguin_drop[['Sex']] = imputer.fit_transform(penguin_drop[['Sex']]) # 때-움

됐으. 이제 동위원소 때우러 가자.

 

num_cols = ['Delta 15 N (o/oo)', 'Delta 13 C (o/oo)'] # 일단 떄울 칼럼

imputer = KNNImputer(n_neighbors=5) # 얘는 그 이웃 참고해서 때워주는 친구입니다
penguin_drop[num_cols] = imputer.fit_transform(penguin_drop[num_cols]) # 때-움

이것도 걍 KNN으로 때웠고, 이제 남은건 범주형 변수 인코딩하고 수치형 변수 스케일러 적용하는거다.

 

스탠다드 스케일러 

scaler = StandardScaler() # 스케일러가 요기잉눼?

num_features = ['Culmen Length (mm)', 'Culmen Depth (mm)', 'Flipper Length (mm)',
                'Body Mass (g)', 'Delta 15 N (o/oo)', 'Delta 13 C (o/oo)'] # 스케일러
cat_features = ['Island', 'Sex'] # 인코더

preprocessor = ColumnTransformer(
    transformers=[
        ('num', StandardScaler(), num_features),
        ('cat', OneHotEncoder(handle_unknown='ignore'), cat_features)
    ])

X_processed = preprocessor.fit_transform(penguin_drop)

일단 저 칼럼 트랜스포머가 뭐 하는 친구인지는 모르겠고 저게 다 된 거 맞습니다. 이대로 학습 들어가면 되는'데'...

 

이제 학습해야징 히히

y = penguin_drop['Species'] # 문제지
X_train, X_test, y_train, y_test = train_test_split(X_processed, y, test_size = 0.2, random_state=42, stratify=y) # 기본값 8:2

print(f'전체 데이터의 수 : {len(X_processed)}')
print(f'학습 데이터의 수 : {len(X_train)}')
print(f'테스트 테이터의 수 : {len(X_test)}')

있어봐요 우리 모델도 아직 못골랐어... 일단 나눈거야... 저거 참고로 비율은 보통 5:3:2가 국룰인데, 5는 학습용이고 3은 과적합 여부 확인용, 2가 테스트용이다.

 

랜덤포레스트

forest = RandomForestClassifier(random_state=42) # 얘는 근데 숲이랑 뭔 상관이 있길래 이름이 랜덤포리스트인겨
forest.fit(X_train, y_train) # 학습
forest_pred = forest.predict(X_test)

이게 다냐고? 예. 구글링했더니 뭐가 막 장황하게 나오긴 했는데 기본적으로는 이게 다다.

 

# 얼마나 맞췄는지 점수(%) 확인
print(f"정확도: {accuracy_score(y_test, forest_pred):.3f}")

# 종별로 얼마나 잘 분류했는지 상세 리포트
print(classification_report(y_test, forest_pred))
정확도: 0.986
                                           precision    recall  f1-score   support

      Adelie Penguin (Pygoscelis adeliae)       0.97      1.00      0.98        30
Chinstrap penguin (Pygoscelis antarctica)       1.00      0.93      0.96        14
        Gentoo penguin (Pygoscelis papua)       1.00      1.00      1.00        25

                                 accuracy                           0.99        69
                                macro avg       0.99      0.98      0.98        69
                             weighted avg       0.99      0.99      0.99        69

????? 아니 이게 내가 예상했던것보다 너무 잘나왔는데? 나 한 80퍼 되나 했는데 이게 뭐시여?????? 되게 지금 당황스러운데? 근데 아델리펭귄에 대한 정밀도는 좀 떨어지는데, 이게 그 결측값때문일수도 있다.

 

이 모델이 펭귄을 분류하는 데 있어서 중요하게 생각한 게 뭘까? 에 대한 답. 부리 길이와 날개 길이, 그리고 13-탄소가 피쳐 TOP 3인데... 저 동위원소 뭔데요? 저거 먹이활동같은거 추적할때 넣는 방사성 동위원소입니다.

 

SVM(서포트 벡터 머신)

svm_model = SVC(kernel='rbf', C=1.0, random_state=42)
svm_model.fit(X_train, y_train)
svm_predictions = svm_model.predict(X_test)
# 1. 성적표 (Classification Report)
print("--- SVM 분류 성적표 ---")
print(classification_report(y_test, svm_predictions))

# 2. 오답 분석 (Confusion Matrix)
cm = confusion_matrix(y_test, svm_predictions)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=svm_model.classes_)
disp.plot(cmap='viridis')
plt.title("SVM Confusion Matrix")
plt.show()
--- SVM 분류 성적표 ---
                                           precision    recall  f1-score   support

      Adelie Penguin (Pygoscelis adeliae)       1.00      1.00      1.00        30
Chinstrap penguin (Pygoscelis antarctica)       1.00      1.00      1.00        14
        Gentoo penguin (Pygoscelis papua)       1.00      1.00      1.00        25

                                 accuracy                           1.00        69
                                macro avg       1.00      1.00      1.00        69
                             weighted avg       1.00      1.00      1.00        69

어… 이게… 이렇게 됨?

 

이거는… 걍 히트맵 그리는게 나을듯… 저 선 진짜 거슬려요.

 

XGBoost

# 1. 변환기 생성
le = LabelEncoder()

# 2. 정답지(y)를 숫자로 변환
y_train_encoded = le.fit_transform(y_train)
y_test_encoded = le.transform(y_test)

# 다중 분류이므로 objective를 'multi:softmax'로 설정하는 게 정석입니다
xgb_model = XGBClassifier(objective='multi:softmax', n_estimators=50, random_state=42)
xgb_model.fit(X_train, y_train_encoded)

# 결과 확인
print(classification_report(y_test, le.inverse_transform(xgb_model.predict(X_test))))

롸? 라벨인코더 쟤는 왜 나옴? 쟤는 답지도 인코딩해줘야 학습합니다... 그리고 XGBoost랑 LightGBM은 싸이킷런에 없으니까 따로 설치하십쇼.

 

                                           precision    recall  f1-score   support

      Adelie Penguin (Pygoscelis adeliae)       0.97      1.00      0.98        30
Chinstrap penguin (Pygoscelis antarctica)       1.00      0.93      0.96        14
        Gentoo penguin (Pygoscelis papua)       1.00      1.00      1.00        25

                                 accuracy                           0.99        69
                                macro avg       0.99      0.98      0.98        69
                             weighted avg       0.99      0.99      0.99        69

랜덤포레스트랑 비슷한디…?

 

LightGBM

얘도 설치하셔야됩니다… lightbgm 쳐놓고 왜 못깔지 이러고 있었음…ㅋㅋㅋㅋ

 

lgbm_model = lgb.LGBMClassifier(n_estimators=10, random_state=42, verbose=-1)
lgbm_model.fit(X_train, y_train_encoded)

# 1. 모델이 예측한 값(숫자)을 받아옵니다
lgb_pred_encoded = lgbm_model.predict(X_test)

# 2. 숫자를 다시 원래 펭귄 이름(문자열)으로 돌립니다
# 여기서 le(LabelEncoder)가 아까 학습(fit)된 상태여야 합니다
lgb_pred = le.inverse_transform(lgb_pred_encoded)

쟤는 좀 적죠…? 50번 돌렸더니 10번만에 여까지해 됐어 퍼펙트해 하고 모델이 GG쳤음…

 

print("--- LightGBM 분류 성적표 ---")
print(classification_report(y_test, lgb_pred))

# 3. 마지막 혼동 행렬 시각화
cm = confusion_matrix(y_test, lgb_pred)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=lgbm_model.classes_)
disp.plot(cmap='Greens')
plt.title("LightGBM Confusion Matrix")
plt.show()
--- LightGBM 분류 성적표 ---
                                           precision    recall  f1-score   support

      Adelie Penguin (Pygoscelis adeliae)       0.94      0.97      0.95        30
Chinstrap penguin (Pygoscelis antarctica)       1.00      0.86      0.92        14
        Gentoo penguin (Pygoscelis papua)       0.96      1.00      0.98        25

                                 accuracy                           0.96        69
                                macro avg       0.97      0.94      0.95        69
                             weighted avg       0.96      0.96      0.96        69

빨리 GG친 것 치고는 니가 성적이 제일 꼴찌여 이자식아...

 

근데 그럴수밖에 없는게 XGBoost나 LightGBM 둘 다 스케일이 큰 애들에 특화되어 있습니다. 그 미국에서 호미 엄청 사가는거 아십니까? 거기는 막 옥수수밭에서 길 잃어먹는 사람도 있을 정도로 밭이 커갖고 농기구들도 스케일이 장난 아니예요. 근데 그게 호미랑 뭔 상관이냐고? 그 농기구들로 정원 손질하려니 세밀하게 안되는거지 이제. 잡초 뽑으려다가 엄한 꽃도 다치고 그러거든요. 근데 호미는 미쿡 농기구 스케일에 비하면 스케일도 작지, 하나로 다 하지… 그거임다.

 

아래 두 개는 입장에서 펭귄 데이터갖고 분류하라는건 미쿡 농기구로 화분에 꽃심는격임.

반응형

'Coding > EDA' 카테고리의 다른 글

Medical Cost Personal Datasets  (0) 2026.03.18
Red Wine Quality  (0) 2026.03.13
Google Play Store – Most Downloaded Android Apps  (0) 2026.03.02
얘! clinvar도 EDA가 된단다! (3)  (0) 2026.02.18
얘! clinvar도 EDA가 된단다! (2)  (0) 2026.02.17
반응형

이거는 말 그대로 플레이스토어 앱 정보가 있는 데이터인데… 본인 아이폰 씁니다. 근데 왜 플레이스토어죠? 앱스토어 데이터가 없다.

 

참고로 전처리 할거 꽤 있으니까 잘 따라오십쇼.


전처리

가격 정상화

가격 정상화는 투트랙으로 이뤄질건데, 일단 앱 가격에 붙어있는 $를 다 빼고 float으로 만들어줄거다. 그리고 결측값도 채워줄건데, 결측값이 있는 앱들은 다 무료 앱이라 0으로 때울거다.

 

# 일단 저 달러부터 떼보시죠 
playstore_df['Price']
0       NaN
1       NaN
2       NaN
3       NaN
4       NaN
      ...  
82    $4.99
83    $3.49
84    $6.99
85    $4.99
86    $3.99
Name: Price, Length: 87, dtype: object

저기 가격 붙어있는거 옆에 달러를 다 떼줄겁니다. ㅇㅋ? ㅇㅇㅋ.

 

playstore_df['Price'] = playstore_df['Price'].str.replace('$', '', regex=False)
playstore_df['Price']
0      NaN
1      NaN
2      NaN
3      NaN
4      NaN
      ... 
82    4.99
83    3.49
84    6.99
85    4.99
86    3.99
Name: Price, Length: 87, dtype: object

그럼에도 아직 오브젝트인 이유는 일단 결측값이 있어서가 아닐까 하는 합리적인 의심을 해봅니다.

 

# 일단 가격이 결측값이 0인 모든 앱의 가격들이 다 무료인지 봅시다. 
free_index = playstore_df.query('Price.isna()').index

for idx in free_index:
    print(playstore_df['Type'].loc[idx])

0으로 때웁시다. 다 무료네.

 

playstore_df['Price'] = playstore_df['Price'].fillna(0) # 채우고 
playstore_df['Price'] = pd.to_numeric(playstore_df['Price']) # 바꾸면

playstore_df['Price'] # 정상화 끝

때운 다음 형변환도 해줘서 이제 float입니다.

 

날짜 형변환

# 날짜가 두개지요? 
# 난 저 리치드가 제출일인 줄 알았는데 일고보니 특정 다운로드 수에 도달한 날짜였음... 그래서 리치드가 퍼블리시드보다 뒤 시점입니다. 
playstore_df['Date_Reached'] = pd.to_datetime(playstore_df['Date_Reached'], errors='coerce')
playstore_df['Date_Published'] = pd.to_datetime(playstore_df['Date_Published'], errors='coerce')

에러나서 봤더니 언노운이 껴있더라고?

 

년도 추출

# 발매년도
playstore_df['Release_year'] = playstore_df['Date_Published'].dt.year
playstore_df['Release_year']

대체 NaT 껴있는거랑 플로트랑 뭔 상관인거임?

 

마일스톤 도달까지 걸린 일수

# 이걸로 되는겨? 
diff = playstore_df['Date_Reached'] - playstore_df['Date_Published']
playstore_df['To_reach'] = diff.dt.days

이거 데이터프레임에는 n days로 표기되는데 타입은 데이트타임임다.

 

설마 이상한거 없겠지

playstore_df.query('To_reach < 0')
App	Developer	Downloads	Date_Reached	Date_Published	Category	Pre_installed	Type	Price	Release_year	To_reach
7	Android Accessibility Suite	Google	10B+	2022-12-18	NaT	Accessibility tool	Yes	Free	0.00	NaN	-9223372036854775808
19	Google Hangouts	Google	5B-10B	2021-06-03	NaT	Communication	NaN	Free	0.00	NaN	-9223372036854775808
55	Duolingo	Duolingo Inc.	500M-1B	NaT	NaT	Games & education	No	Free	0.00	NaN	-9223372036854775808
78	Camera ZOOM FX Premium	androidslide	1M-5M	2012-03-19	NaT	Photo editor	No	Paid	4.99	NaN	-9223372036854775808

 

아뇨 있는데요? 

 

# 날립시다. 
playstore_df = playstore_df.dropna()
playstore_df

저 데이로 매겨야되는데 결측값 껴있으면 거시기하니까 날리자 걍.


오케이 렛츄고

마일스톤별 분석

특정 마일스톤별로 묶어서 한번 봅시다.

# 마일스톤별 그룹화 
playstore_df.groupby('Downloads').size()
Downloads
10B+       16
10M+        5
1B-5B      24
1M-5M       9
500M-1B    11
5B-10B     12
5M-10M      6
dtype: int64

얘는 전처리 감도 안오더라… 여기서 마일스톤 그룹별로는 어떤 앱이 가장 빨리 달성했을까?

 

가장 빨리 마일스톤을 달성한 앱

# 마일스톤별로 묶은 다음 최솟값의 인덱스를 추출 
min_idx = playstore_df.groupby('Downloads')['To_reach'].idxmin()

# 오케이 렛츠씨 
playstore_df.loc[min_idx]

와 게임이 두개나 있는데 내가 모르는 게임이야...

 

마일스톤 달성에 오래걸린 앱

# 마일스톤별로 묶은 다음 최솟값의 인덱스를 추출 
max_idx = playstore_df.groupby('Downloads')['To_reach'].idxmax()

# 오케이 렛츠씨 
playstore_df.loc[max_idx]

인별이 오래걸린건 좀 의외다. 구글번역기... 음... 사실 요즘 한국인들은 다 파파고 씁니다...

 

게임! 게임을 보자! 

game_df = playstore_df.query('Category.str.contains("Game")')
game_df

왜 쿼리에 저게 들어가냐고요? 카테고리가 게임, 어쩌고 게임 & 저쩌고 다 이난리라서요. 진짜 정규화 마려웠음.

 

# 마일스톤별로 묶은 다음 최솟값의 인덱스를 추출 
min_idx = game_df.groupby('Downloads')['To_reach'].idxmin()

# 오케이 렛츠씨 
game_df.loc[min_idx]

슈터가 세개지요…

 

# 마일스톤별로 묶은 다음 최솟값의 인덱스를 추출 
max_idx = game_df.groupby('Downloads')['To_reach'].idxmax()

# 오케이 렛츠씨 
game_df.loc[max_idx]

포고는 솔직히 느그언틱이 뻘짓만 덜했어도 좀 더 빨리 도달했을것같은데. 

 

마인크래프트나 로블록스나 다들 애들이 좋아하는 게임이간 한데, 마인크래프트는 '돈을 내고' 게임을 사야 하고 로블록스는 설치는 공짜고 인게임 안에서 뭘 사야 하는 구조다. 그럼 유료게임들이 다 마일스톤 달성이 오래 걸리냐 하면 그것도 아닌게, 위에 빨리 달성한 게임들에도 유료게임이 있어요. 게임이 오래 가려면 재미와 게임성도 중요하지만 운영을 X같이 하면 안됩니다.

 

무료앱

# 마일스톤별로 묶은 다음 최솟값의 인덱스를 추출 
min_idx = free_df.groupby('Downloads')['To_reach'].idxmin()

# 오케이 렛츠씨 
free_df.loc[min_idx]

일단 유튜브 뮤직은 한번도 쓴적 없지만 가끔 Flo는 쓴다. 근데 이것도 요금제 무료로 주는거 있어서 쓰는거지 보통은 잘 안 씀. 나는 음원 다운로드해서 폰에 넣고다닌다. 애초에 내가 듣고 다니는 노래 중에 특정 음원사이트에는 수록 안 되는 곡도 있고(특히 멜론) 대부분 요금이 구독제거든.. 내가 구독하는건 포홈 박스(1년에 2만원)랑 닌스온, 드롭박스, 아이클라우드가 다다.

 

그리고 유튜브는 이것들 광고 꼬라지가 X같아서 구독하기 싫음. 막말로 얘네 돈만 많이 주면 나도 광고해줄걸?

 

# 마일스톤별로 묶은 다음 최솟값의 인덱스를 추출 
max_idx = free_df.groupby('Downloads')['To_reach'].idxmax()

# 오케이 렛츠씨 
free_df.loc[max_idx]

구글번역기... 음... 내 중고딩 시절에는 번역이 발퀄이라 안 썼고 요즘은 파파고나 DeepL이 잘 되어있어서 안씁니다...

 

유료앱

# 마일스톤별로 묶은 다음 최솟값의 인덱스를 추출 
min_idx = paid_df.groupby('Downloads')['To_reach'].idxmin()

# 오케이 렛츠씨 
paid_df.loc[min_idx]

얘네들 뭐 하는 게임임? 한번도 못본거같은데..

 

# 마일스톤별로 묶은 다음 최댓값의 인덱스를 추출 
max_idx = paid_df.groupby('Downloads')['To_reach'].idxmax()

# 오케이 렛츠씨 
paid_df.loc[max_idx]

문제는 얘들도 뭐 하는 애들인지 모르겠음.

 

년도별로 보기

2010~2020

참고로 2010년도 이전 앱도 있긴 있습니다. 근데 내가 갤럭시 A를 본 게 2010년인데? 뭔 경우냐 이건?

 

year_min_idx = playstore_df.query('2010 <= Release_year < 2021').groupby(['Release_year'])['To_reach'].idxmin()
playstore_df.loc[year_min_idx]

음... 스타듀밸리는 그럴만 했지. 후르츠 닌자도 예전에 스마트폰이나 어른패드에서 많이 했던 게임 중 하나다. 일단 저 게임은 룰이 대단히 간단한데, 과일만 싹둑해야 합니다. 근데 그 과일만 싹둑하는 작업이 은근 어려운게 포인트임. 염소 시뮬레이터는... 그래요.. 걔들은 약 빨고 게임을 만들었어...

 

year_max_idx = playstore_df.query('2010 <= Release_year < 2021').groupby(['Release_year'])['To_reach'].idxmax()
playstore_df.loc[year_max_idx]

우리 그 클래스챗이 따로 있는데 거기서 가끔 문제가 터집니다. 마이크가 뻑나서 본의아니게 다리를 얻고 목소리를 잃은 인어공주가 되기도 하고, 소리가 안들려서 베토벤이 이 상황에서 작곡을 했구나를 깨닫기도 함. 그럴때 대안으로 쓰는게 구글미트예요. 많이 쓰임.

 

스타듀밸리는… 2019년에 설마 저거 하나 들어간거 아니지?

 

2021~

2021년 이후는 코드 올리고 자시고 할 것도 없는게, 이게 답니다. 데이터 원본 행 수가 좀 빈약함…

 

모스트 다운로드 오브 구글 앱

google_df = playstore_df.query('Developer.str.contains("Google") and Downloads == "10B+"')
google_df

B가 10억이니까 쟤들은 최소 100억번 이상 다운로드 됐다는 얘기가 되겠죠…

 

google_df_s = google_df.sort_values('To_reach', ascending=False)
sns.barplot(google_df_s, x = 'App', y = 'To_reach', hue = 'App', palette='viridis')
plt.title('To reach for milestone: Google apps')
plt.xlabel('App')
plt.xticks(rotation = 45)
plt.ylabel('To Reach (days)')
plt.show()

저 안드로이드 스위치 뭐 하는 앱인지 아시는 분은 제보 바랍니다.

 

모스트 다운로드 오브 게임

most_game_df = playstore_df.query('Category.str.contains("Games") and Downloads.str.contains("B")')
most_game_df = most_game_df.sort_values(['Downloads','To_reach'], ascending = [True, False])
sns.barplot(most_game_df, x = 'App', y = 'To_reach', hue = 'Downloads', palette='viridis')
plt.title('To reach for milestone: Games')
plt.xlabel('App')
plt.xticks(rotation = 45)
plt.ylabel('To Reach (days)')
plt.show()

내 언젠가 저 세팅도 화이트그리드로 싹 바꿀것이다…

 

근데 로블록스가 애들한테 인기있는 게임 아니었나? 생각보다 도달하는데 오래걸렸다?

반응형

'Coding > EDA' 카테고리의 다른 글

Red Wine Quality  (0) 2026.03.13
Palmer Archipelago (Antarctica) penguin data  (0) 2026.03.11
얘! clinvar도 EDA가 된단다! (3)  (0) 2026.02.18
얘! clinvar도 EDA가 된단다! (2)  (0) 2026.02.17
얘! clinvar도 EDA가 된단다! (1)  (0) 2026.02.15
반응형

있는것도 복잡한데 저걸 왜 넣냐고요? 라이노바이러스는 좀 덜한데, 이게 기본적으로 300개씩 찾고 그렇다보니 계통수가 무지하게 길어집니다. 이러면 이걸 넣는 나도 고통이고 읽는 사람도 고통이예요. 거의 뭔 스크롤이여 스크롤. 근데 계층적 군집분석 결과가 덴드로그램인데 이거 목 꺾고 옆으로 보면 계통수거든요? 그리고 어쨌든 묶은거니까 이거 넣어보자 해서 넣었죠.


실루엣 계수

이게 원래는 군집 내에서의 응집도와 다른 군집간의 거리를 비교해서 군집분석이 잘 됐는지, 안 됐는지를 평가하는 지표인데 k-means나 k-medoid에서 군집 개수 나눌때도 쓴다. 그 개수가 돌려돌려 돌림판으로 나오는게 아닙니다… 그럼 계통수는 버리는건가요? 아니, 그거 보고 대충 개수 나눌수도 있다.

 

한타바이러스 실루엣 계수

이거 봐봐요 이걸로 뭘 어떻게 정할거야… 저거 계통수 산출한거에서 덩어리 수 보고 정했어요 결국…

 

k-medoid

아니 근데 왜 하필 저놈임? 일단 계통수를 만들고 쟤까지 진행하는거라 거리행렬이 준비되어있는데, k-medoid는 그 거리행렬을 주기만 하면 됩니다. 그리고 서열이 이미 준비되어있으니 대표서열로 걍 하면 되고, 이친구는 중앙값으로 하는거라 이상치에 영향을 덜 받는다.

 

아니 우리 거리행렬이 있었어요?

# 1. 거리 계산
calculator = DistanceCalculator('identity')
dm = calculator.get_distance(alignment)

이거 찾수?

 

인플루엔자 K-medoid

인플루엔자는 한 '아종' 안에서 갈라지는거고 코로나바이러스는 스파이크만 해서 트리 관련 통계가 한타, 라이노랑 다르지만 이건 일단 계통수만 도출할 수 있으면 다 그릴 수 있기 때문에 계통수를 대체할 수 있다. 단, 쿼리를 잘 짜야 한다는 거… 저기 혼자 떨어진 점 보여요? 저거 partial CDS인데 쟤 끼면 군집 분포가 이상해진다.

반응형

'Coding > Python' 카테고리의 다른 글

Polars 데이터프레임도 시각화가 되나요?  (0) 2026.04.10
Polars를 써보자  (0) 2026.04.09
M1V1 = M2V2  (0) 2026.02.15
코로나바이러스 MSA  (0) 2026.02.05
라이노바이러스 유전자로 MSA를 해보았다  (0) 2026.01.27
반응형

일단 미리 말씀드리자면 결과 진짜 심각하게 시망했음…ㅋㅋㅋㅋㅋㅋ 걍 이렇게 하는구나만 알아두세요…

# 군집분석용
from scipy.cluster.hierarchy import dendrogram, linkage
from scipy.cluster.hierarchy import fcluster
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import StandardScaler # PCA에서 많이 보인 그 분
from sklearn.metrics import pairwise_distances # 저친구가 거리행렬을 안주면 유혈사태(아니고 에러사태)가 납니다

# k-means
from sklearn.cluster import KMeans

# k-medoid (이거 따로 까서야돼요)
import kmedoids

# 누구세요?
from sklearn.manifold import TSNE

# 통계분석용
from scipy import stats

일단 넘파이 판다스 맷플롭 씨본에 쟤들 추가로 부르시고.


Hierarchical clustering

여기 어딘가에 있는 바이오파이썬 하다가 나온 그거 맞습니다. 이건 하게되면 결과물(?)이 덴드로그램으로 나와요.

 

data = shopping_df[['Age']] # 나이
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data) # 를 스케일링합니다
category = np.array(shopping_df['Payment Method']) # 구매 방법
category = category.reshape(-1, 1)

encoder = OneHotEncoder().fit(category) ## 범주와 One-Hot Encoding간 매핑 생성
sparse_mat = encoder.transform(category) ## 실제로 변환할 때에는 transform 사용
sparse_mat = sparse_mat.toarray()
combined_data = np.hstack([data_scaled, sparse_mat]) # 파이널- 퓨-전!!!

나이는 수치형이라 스케일러 돌렸고, 구매 수단은 범주형이라 원 핫 인코딩 돌렸다. 그리고 합치면 준비 끝.

 

아 저 그리드 진짜 꼴뵈기싫어... ㅡㅡ 아무튼 이게 덴드로그램이다. 목을 오른쪽으로 90도 꺾고 보시면 그게 계통수입니다.

 

k-means&k-medoid

# 1. Inertia(오차 제곱합) 값을 저장할 리스트
inertia = []
k_range = range(1, 11)  # 1개부터 10개까지 군집 개수를 늘려가며 확인

# 2. 반복문으로 각 K에 대한 모델 학습
for k in k_range:
    # n_init=10: 초기 중심점을 10번 다르게 잡아서 최적의 결과를 선택
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(combined_data) # 나이(Scaled) + 결제수단(One-Hot) 데이터
    inertia.append(kmeans.inertia_)

# 3. 그래프 시각화 (설정하신 NanumSquare가 적용됩니다)
plt.figure(figsize=(10, 6))
plt.plot(k_range, inertia, marker='o', color='forestgreen', linewidth=2)
plt.title('Optimal K 찾기 (Elbow Method)', fontsize=15)
plt.xlabel('군집 개수 (K)', fontsize=12)
plt.ylabel('Inertia (오차 제곱합)', fontsize=12)
plt.xticks(k_range)
plt.grid(True, alpha=0.3)
plt.show()

저 k가 코리안의 k가 아니고 군집 개수다. 근데 그 군집 개수를 고스톱 쳐서 정하는 게 아니고 정하는 방법이 여러개가 있습니다. 여기서는 그 뭐라해야되지? 주성분분석때도 주성분 고른다고 했었던 그 엘보 찾는걸로 할겁니다.

 

저 꺾은선을 보십시오. 2가 엘보다. 왜죠? 2에서 그래프의 기울기가 급격하게 변하거든요. 이렇게 되면 아 군집은 두개가 적당하구나... 하시면 된다.

 

# K-Means
kmeans4 = KMeans(n_clusters=4, random_state=42, n_init=10)
shopping_df['KMeans_K4'] = kmeans4.fit_predict(combined_data)

# 거리 행렬 & K-Medoids
dist_matrix = pairwise_distances(combined_data, metric='euclidean')
km_model = kmedoids.KMedoids(n_clusters=4, method='fasterpam', random_state=42)
km_result = km_model.fit(dist_matrix)
shopping_df['KMedoids_K4'] = km_result.labels_

# 대표 고객
medoid_indices = km_result.medoid_indices_
representative_customers = shopping_df.iloc[medoid_indices]
print(representative_customers[['Age', 'Purchase Amount (USD)']])

한 블럭에 둘을 같이 돌렸는데.. 근데 둘이 뭔 차이임? k가 군집의 개수인 건 같은데, k-means는 평균이 기준이고 k-medoid는 중앙값이 기준입니다. 이렇게까지 나눠야 하나 싶으실텐데, 평균이 이상치의 영향을 되게 많이 받아요.

 

여러분 서울대에서 졸업생들 평균 연봉이 제일 높은 학과가 어딘지 아십니까? 공대? 아님. 의대? 아님. 서울대 사학과임. 아니 문사철의 그 사요? 재드래곤이랑 용진이횽이 거기 나와서 그렇게 된거임. 저 둘의 연봉이 어떻게 보면 이상치가 돼서 평균 연봉에 영향을 끼치는거예요.

 

그럼 이게 왜 시망한건지 알려드리겠음. 이게 원래는 이렇게 안 나오고 점들이 일정 구역에 뭉쳐있습니다. 스타팅 포켓몬(최종진화체)으로 군집을 만든다 치면 불타입은 불타입끼리, 물타입은 물타입끼리, 풀타입은 풀타입끼리, 그리고 노랑뚱띠랑 이브이 따로 있을거란 말이죠. 그 안에서도 최종진화체가 단일타입인지 이중타입인지 혹은 종족값 이런걸로 세분화는 되겠지만 기본적으로 이 점들이 뭉쳐있어야 합니다.

 

이거는 MSA 얘기하면서 다시 얘기하겠지만 일단 이렇게 나와야 해요.

반응형
반응형

그렇다. 대망의 2부가 돌아왔다.

 

이게 정보 확인하는거 생략하고도 분량 꽤 되니까 알아서 쫓아오십쇼. 다음편에 태블로 얘기만 할거라서 이번편에 다 끝낼거임.


전처리

쓸 칼럼만 추리기

이게 칼럼이 되게 많은데 그걸 우리가 다 쓸 게 아니거든요? 그래서 쓸 것만 추린 다음에 데이터프레임을 재구성하고 그걸 csv파일로 보내야 합니다. 왜냐고? 그걸 보내야 태블로에서도 쓰죠.

analysis_column = ['CHROM','POS','REF','ALT','CLNSIG','CLNVC','GENEINFO','CLNREVSTAT'] # 칼럼 뭐하는건지 위에 있어요

1. CHROM: 염색체(몇 번 염색체인지)
2. POS: 염색체 어디?
3. REF, ALT: 비포&애프터 (REF에 있는 시퀀스가 ALT로 바뀐 변이다)
4. CLNSIG: 임상적 유의성
5. CLNVC: 변이 타입(얘가 껴들어간겨 빠진겨 바뀐겨 뒤집어진겨)
6. GENEINFO: 유전자 이름+Entrez ID
7. 별점(왜 있는거냐)

 

이 칼럼들만 갖고와서

analysis_column = ['CHROM','POS','REF','ALT','CLNSIG','CLNVC','GENEINFO','CLNREVSTAT'] # 칼럼 뭐하는건지 위에 있어요
clinvar_df_analysis = clinvar_df[analysis_column]

clinvar_df_analysis

이렇게 하면 데이터프레임 재구성은 끝난다.

 

그러고도 결측값이 왜 있는거냐고

이러고도 결측값이 있는 이유는 아직 연구가 덜 됐기 때문이다. 내가 isna()./sum()으로 확인해보고 원본까지 대조해본 결과임.

 

fill_values = {
    'CLNSIG': 'Unknown_Significance',
    'CLNREVSTAT': 'No_Assertion',
    'GENEINFO': 'Unknown_Gene',
    'MC': 'Unknown_Consequence'
}

clinvar_df_analysis = clinvar_df_analysis.fillna(value=fill_values)

clinvar_df_analysis['GENE_SYMBOL'] = clinvar_df_analysis['GENEINFO'].apply(
    lambda x: x.split(':')[0] if ':' in x else x
)

그래서 이게 최선이었습니다. 아, 하는 김에 유전자 이름도 분리함.

 

CLNSIG 범주화

# # 묶기 위한 조건 설정
conditions = [
    clinvar_df_analysis['CLNSIG'].str.contains('Pathogenic|Likely_pathogenic', case=False, na=False),
    clinvar_df_analysis['CLNSIG'].str.contains('Benign|Likely_benign', case=False, na=False),
    clinvar_df_analysis['CLNSIG'].str.contains('Uncertain_significance|VUS', case=False, na=False),
    clinvar_df_analysis['CLNSIG'].str.contains('Conflicting', case=False, na=False),
    clinvar_df_analysis['CLNSIG'].str.contains('risk_factor|drug_response|association|protective|Affects', case=False, na=False)
]

# 결과 그룹명
choices = ['Pathogenic', 'Benign', 'VUS', 'Conflicting', 'Risk/Other']

# 기본값은 Unknown으로 설정
clinvar_df_analysis['CLNSIG_Group'] = np.select(conditions, choices, default='Unknown')

# 결과 확인
print(clinvar_df_analysis['CLNSIG_Group'].value_counts())

나노 반도체 단위로 나뉘어진 CLNSIG을 대충 간소화했다. 여기까지 하고 to_csv로 저장해주면 태블로에서도 불러올 수 있습니다.

 

분석 드가자

CLNSIG별로 보기

clnsig_group = clinvar_df_analysis.groupby('CLNSIG_Group')['CLNSIG_Group'].count().sort_values(ascending=False)
clnsig_group

VUS는 말 그대로 몰?루인거고 Benign은 변이는 변이인데 누구나 하나쯤은 다 갖고 있는 뭐 그런거다. 그리고 그 다음으로 많은 게 Pathogenic이다.

 

ax = sns.barplot(clnsig_group)

plt.title('CLNSIG Group에 따른 변이 수')
plt.xlabel('CLNSIG Group')
plt.yscale('log') # 이거 안하면 막대기 하나 안보임

for container in ax.containers:
    # fmt='%d'는 정수로 표시, label_type='edge'는 막대 끝에 표시
    ax.bar_label(container, fmt='%d', padding=3, fontsize=10)

plt.tight_layout()
plt.show()

아… 나도 막대기 색을 나누고 싶었는데요… 아… 이게… size()로 했더니 시리즈가 돼서 애가 인식을 못해……

 

염색체 종류별 CLNSIG

아니 그럼 1번부터 다 보나요? 놉. 아까 내가 썼는지 모르겠는데 염색체도 범주화했다. 상염색체(1~22), 성염색체(XY), 미토콘드리아(얘는 지꺼 따로 있음)+언노운 이렇게 있음. 전에도 얘기했지만 1번부터 다 본다? 하나씩 그리면 스크롤이 너무 길고 그렇다고 모으자니 그래프가 뵈지도 않아요.

clnsig_chr_group = clinvar_df_analysis.groupby(['CHROM_Type','CLNSIG_Group']).size().unstack().fillna(0)
clnsig_chr_group
ax = clnsig_chr_group.plot(kind='bar', stacked=True, ax=plt.gca(), color=sns.color_palette("Purples_r", n_colors=5))
plt.title('염색체 그룹에 따른 CLNSIG 그룹 수')
plt.xlabel('염색체')
plt.xticks(ticks=[0, 1, 2, 3], labels=['상염색체','미토콘드리아','성염색체','불명'])
plt.yscale('log') # 이거 안하면 막대기 하나 안보임
plt.tight_layout()
plt.show()

성염색체와 달리 상염색체와 미오콘드리아는 Benign 다음으로 VUS가 두드러지게 많다. 성염색체는 이렇게 보면 비슷비슷해보는데 차이가 존재하긴 함.

 

Pathogenic의 비중

clnsig_chr_group = clinvar_df_analysis.groupby(['CHROM_Type','CLNSIG_Group']).size().unstack().fillna(0)
clnsig_chr_group['total'] = clnsig_chr_group.sum(axis=1)
clnsig_chr_group['Pathogenic rate'] = round(clnsig_chr_group['Pathogenic'] / clnsig_chr_group['total'] * 100, 2)
clnsig_chr_group

뭔가… 비중이 생각보다 얼마 안됨… 아, Pathogenic은 발병시키는, 병원(호스피털 말고)성의 뭐 그런 뜻이다. 그니까 패소제닉한건 터지면 병되는건데 이게 유전자 바이 유전자지만 암이 되는 경우도 있고, 유전병이 되는 경우도 있다.

 

ax = sns.barplot(clnsig_chr_group, x = 'CHROM_Type', y = 'Pathogenic rate', hue='CHROM_Type')
plt.title('염색체별 Pathogenic 비율')
plt.xlabel('염색체')
plt.ylabel('Pathogenic 비율 (%)')
plt.xticks(ticks=[0, 1, 2], labels=['상염색체','미토콘드리아','성염색체'])
plt.xlim(-0.5, 2.5)

for container in ax.containers:
    # fmt='%d'는 정수로 표시, label_type='edge'는 막대 끝에 표시
    ax.bar_label(container, fmt='%.2f', padding=3, fontsize=10)

# 얘는 로그 빼도 됩니다. 백분율이라;;
plt.tight_layout()
plt.show()

불명은 아예 0이라 축 조절하면서 빼버림… 저 축 틱이 0부터 2까지라고 범위도 0부터 2까지로 하시면 막대들이 양 옆으로 달라붙습니다. 항상 여유 범위를 주십시오.

 

Pathogenic한 변이들

clnsig_pathogenic = clinvar_df_analysis.query('CLNSIG == "Pathogenic"') # Pathogenic
clnsig_pathogenic

이제 우리는 병원성 변이들에 주목해보자.

 

clnsig_pathogenic.groupby('CLNVC').size().sort_values(ascending=False)

하필이면 제일 잡기 빡센 놈이 제일 많네… Single nucleotide variant가 뭐냐면… 그… 나비효과 아세요? 나비의 날갯짓이 지구 반대편에서 토네이도 된다는. 딱 그짝이다. 사람 몸 크기에 비하면 DNA 염기가 되게 작거든요. 염색체 안에 저런게 수백 수천만개가 때려박혀져 있는데 그 중에서 고거 하나 바뀐걸로 아미노산이 바뀌고(바꼈는데 같은 아미노산을 지정하는 경우도 있음) 그걸로 단백질 접힘이 바뀌고 접힘이 바뀐 단백질이 몸에 큰 영향을 끼친다.

 

저게 왜 잡기 빡세냐고? 스케일이 작잖아요. 염기가 뭉텅이로 빠진것도 아니고 딱 하나 빠진거잖아요. 마치 쌀알 10000개인 밥과 10001개인 밥을 주고 어떤게 만개게? 하는거랑 비슷하다.

 

ax = sns.barplot(clnsig_pathogenic_nvc)
purple_color = plt.colormaps['Purples'](0.2)

# 모든 막대(patch) 가져오기
for patch in ax.patches:
    # 예: 특정 조건(height가 20 이상인 경우)의 막대만 색상 변경
    if patch.get_height() > 20000:
        pass
    else:
        patch.set_color(purple_color)

for container in ax.containers:
    # fmt='%d'는 정수로 표시, label_type='edge'는 막대 끝에 표시
    ax.bar_label(container, padding=3, fontsize=10)

plt.title('변이 유형별 분포')
plt.ylabel('Count')
plt.show()

SNV는 알겠는데 저 뒤에 두개는 뭐냐… Deletion은 염기가 하나든 여러개든 있었는데 없었습니다 된 거고 Duplication은 시퀀스가 갑자기 원쁠원이 되는거다. 예를 들어서 GAATTC가 하나였다가 두개가 되는 거 말이다. transposon이랑은 다릅니다. 걔는 태생이 이사다니는 놈임.

 

염색체 유형별 분류

clnsig_pathogenic_chrom = clnsig_pathogenic.groupby('CHROM_Type').size().sort_values(ascending=False)
clnsig_pathogenic_chrom
ax = sns.barplot(clnsig_pathogenic_chrom)

for container in ax.containers:
    # fmt='%d'는 정수로 표시, label_type='edge'는 막대 끝에 표시
    ax.bar_label(container, padding=3, fontsize=10)

plt.title('염색체 종류별 분포')
plt.ylabel('Count')
plt.yscale('log')
plt.show()

상염색체는 22개고(1~22) 성염색체는 두개라 그런가…?

 

염색체별 세분류

clnsig_pathogenic_chrom = clnsig_pathogenic.groupby('CHROM').size().sort_values(ascending=False)
clnsig_pathogenic_chrom
ax = sns.barplot(clnsig_pathogenic_chrom[:10])
purple_color = plt.colormaps['Purples'](0.2)

# 모든 막대(patch) 가져오기
for patch in ax.patches:
    # 예: 특정 조건(height가 20 이상인 경우)의 막대만 색상 변경
    if patch.get_height() > 10000:
        pass
    else:
        patch.set_color(purple_color)

for container in ax.containers:
    # fmt='%d'는 정수로 표시, label_type='edge'는 막대 끝에 표시
    ax.bar_label(container, padding=3, fontsize=10)

plt.title('변이 유형별 분포 TOP 10 (염색체별)')
plt.ylabel('Count')
plt.show()

다 보기는 좀 거시기해서 TOP 10만 봤다. 17번, 2번 다음으로 X염색체가 많고 그 다음으로 1, 11번까지가 TOP 5다.

 

clnsig_pathogenic_chrom_Auto = clnsig_pathogenic.query('CHROM_Type == "Autosome"').groupby('CHROM').size().sort_values(ascending=False)
clnsig_pathogenic_chrom_Auto
ax = sns.barplot(clnsig_pathogenic_chrom_Auto[:10])
purple_color = plt.colormaps['Purples'](0.2)

# 모든 막대(patch) 가져오기
for patch in ax.patches:
    # 예: 특정 조건(height가 20 이상인 경우)의 막대만 색상 변경
    if patch.get_height() > 10000:
        pass
    else:
        patch.set_color(purple_color)

for container in ax.containers:
    # fmt='%d'는 정수로 표시, label_type='edge'는 막대 끝에 표시
    ax.bar_label(container, padding=3, fontsize=10)

plt.title('변이 유형별 분포 TOP 10 (상염색체)')
plt.ylabel('Count')
plt.show()

성염색체 빼고 상염색체만 봅시다. 17, 2, 1, 11, 16순으로 많다. 저거는 변이 개수가 10000개 이상인 것만 강조하는거라 저렇게 나온거임…

 

clnsig_pathogenic_chrom_Sex = clnsig_pathogenic.query('CHROM_Type == "Sex_Chrom"').groupby('CHROM').size().sort_values(ascending=False)
clnsig_pathogenic_chrom_Sex
ax = sns.barplot(clnsig_pathogenic_chrom_Sex)
purple_color = plt.colormaps['Purples'](0.2)

# 모든 막대(patch) 가져오기
for patch in ax.patches:
    # 예: 특정 조건(height가 20 이상인 경우)의 막대만 색상 변경
    if patch.get_height() > 10000:
        pass
    else:
        patch.set_color(purple_color)

for container in ax.containers:
    # fmt='%d'는 정수로 표시, label_type='edge'는 막대 끝에 표시
    ax.bar_label(container, padding=3, fontsize=10)

plt.title('변이 유형별 분포 TOP 10 (상염색체)')
plt.ylabel('Count')
plt.yscale('log')
plt.show()

아놔 타이틀 수정해야되네.. 엥? X염색체가 더 많네요? 그 또한 체급차이다. Y염색체는 X염색체에 비해 짤똥하고 들어있는 유전자도 수십개정도지만, X염색체는 8~900개의 유전자가 들어있다.

 

유전자 TOP 10

clnsig_pathogenic_gene = clnsig_pathogenic.groupby('GENE_SYMBOL').size().sort_values(ascending=False)
clnsig_pathogenic_gene
ax = sns.barplot(clnsig_pathogenic_gene[:10])

# 모든 막대(patch) 가져오기
for patch in ax.patches:
    # 예: 특정 조건(height가 20 이상인 경우)의 막대만 색상 변경
    if patch.get_height() > 3500:
        pass
    else:
        patch.set_color(purple_color)


for container in ax.containers:
    # fmt='%d'는 정수로 표시, label_type='edge'는 막대 끝에 표시
    ax.bar_label(container, padding=3, fontsize=10)

plt.title('변이 유형별 분포 TOP 10 (유전자별)')
plt.xlabel('유전자')
plt.ylabel('Count')
plt.show()

BRCA 어디서 들어봤다 그죠? 안젤리나 졸리가 저 유전자에 변이가 있어서 유방을 절제하고 복원했잖음. 아니 왜 그렇게까지 해요? 저기 변이 있으면 유방암, 난소암에 걸릴 확률이 올라갑니다. 그러니까 암이라는 상태이상에 취약해지는 디버프인 셈이다. 이 변이도 유전되기때문에 아마 본인이 BRCA 변이가 있다면 가족중에 유방암이나 난소암에 걸린 사람이 계실 것이다.

 

그렇다고 어씨 나도 BRCA 변이 있네 조졌다 이럴것까진 없음. 우리는 늘 그렇듯이 여러분들이 최대한 건강하게 살 수 있도록 연구할겁니다.

 

SNV TOP 10

clnsig_pathogenic_snv = clnsig_pathogenic.query('CLNVC == "single_nucleotide_variant"').groupby('GENE_SYMBOL').size().sort_values(ascending=False)
clnsig_pathogenic_snv
ax = sns.barplot(clnsig_pathogenic_snv[:10])

# 모든 막대(patch) 가져오기
for patch in ax.patches:
    # 예: 특정 조건(height가 20 이상인 경우)의 막대만 색상 변경
    if patch.get_height() > 1000:
        pass
    else:
        patch.set_color(purple_color)


for container in ax.containers:
    # fmt='%d'는 정수로 표시, label_type='edge'는 막대 끝에 표시
    ax.bar_label(container, padding=3, fontsize=10)

plt.title('SNV가 가장 많은 유전자 TOP 10')
plt.xlabel('유전자')
plt.ylabel('Count')
plt.show()

NF1은 신경섬유종(1형)과 관련 있는 유전자고, FBN1은 찾아보니 피브릴린 1이란다. 마르판 증후군도 FBN1에 문제 생겼을 때 발생하는 병 중 하나다.

 

Deletion TOP 10

clnsig_pathogenic_del = clnsig_pathogenic.query('CLNVC == "Deletion"').groupby('GENE_SYMBOL').size().sort_values(ascending=False)
clnsig_pathogenic_del
ax = sns.barplot(clnsig_pathogenic_del[:10])

# 모든 막대(patch) 가져오기
for patch in ax.patches:
    # 예: 특정 조건(height가 20 이상인 경우)의 막대만 색상 변경
    if patch.get_height() > 1000:
        pass
    else:
        patch.set_color(purple_color)


for container in ax.containers:
    # fmt='%d'는 정수로 표시, label_type='edge'는 막대 끝에 표시
    ax.bar_label(container, padding=3, fontsize=10)

plt.title('Deletion이 가장 많은 유전자 TOP 10')
plt.xlabel('유전자')
plt.ylabel('Count')
plt.show()

TOP 3은 어디서 많이 보셨던 애들이고… ATM은 찾아보니 암 억제 유전자다. 이건 또 뭐고? 암 억제 유전자는 발현되면 암을 막는 유전자고, 암 유전자(옹코진)는 발현되면 암 되는 유전자다. 전자는 브레이크, 후자는 엑셀.

 

Duplication TOP 10

clnsig_pathogenic_du = clnsig_pathogenic.query('CLNVC == "Duplication"').groupby('GENE_SYMBOL').size().sort_values(ascending=False)
clnsig_pathogenic_du
ax = sns.barplot(clnsig_pathogenic_du[:10])

# 모든 막대(patch) 가져오기
for patch in ax.patches:
    # 예: 특정 조건(height가 20 이상인 경우)의 막대만 색상 변경
    if patch.get_height() > 500:
        pass
    else:
        patch.set_color(purple_color)


for container in ax.containers:
    # fmt='%d'는 정수로 표시, label_type='edge'는 막대 끝에 표시
    ax.bar_label(container, padding=3, fontsize=10)

plt.title('Duplication이 가장 많은 유전자 TOP 10')
plt.xlabel('유전자')
plt.ylabel('Count')
plt.show()

일단 여기까지 하고 태블로로 빠졌음.

 

반응형

'Coding > EDA' 카테고리의 다른 글

Google Play Store – Most Downloaded Android Apps  (0) 2026.03.02
얘! clinvar도 EDA가 된단다! (3)  (0) 2026.02.18
얘! clinvar도 EDA가 된단다! (1)  (0) 2026.02.15
Ramen ratings  (0) 2026.02.11
Post-COVID Video Games Worldwide (2021-2025)  (0) 2026.02.10
반응형

이거 3부작입니다... 일단 분석을 하다 말았고, 태블로도 써야됨.


clinvar는 유전적 변이와 인간의 표현형과의 관계에 대한 데이터를 수집하여 보관하는 데이터베이스이다. 이게 데이터가 어떻게 되어있냐면 몇번 염색체 어디에 뭐가 어떻게 뻑나면 어떤 변이더라~ 이런게 들어있는데, vcf파일입니다. 이거 분석하려면 여는것부터 골치아픔. 근데 이게 된다고요?

 

내가 vcf파일 상태로는 열기도 조작하기도 귀찮아서 아예 거기 안에 있는 내용을 데이터프레임화하고 csv로 만드는 코드를 짰음.


VCF파일 내용물

'1', '66926', '3385321', 'AG', 'A', '.', '.', 'ALLELEID=3544463;CLNDISDB=Human_Phenotype_Ontology:HP:0000547,MONDO:MONDO:0019200,MeSH:D012174,MedGen:C0035334,OMIM:268000,OMIM:PS268000,Orphanet:791;CLNDN=Retinitis_pigmentosa;CLNHGVS=NC_000001.10:g.66927del;CLNREVSTAT=criteria_provided,_single_submitter;CLNSIG=Uncertain_significance;CLNSIGSCV=SCV005419006;CLNVC=Deletion;CLNVCSO=SO:0000159;GENEINFO=OR4F5:79501;MC=SO:0001627|intron_variant;ORIGIN=0'

'1', '66926', '3385321', 'AG', 'A', '.', '.'는 위치 정보다. 1번 염색체 66926번째 염기에 생긴 변이고, clinvar 아이디는 3385321이고, AG가 A로 바뀐 변이라는 얘기. 뒤에 점 두개는 QUAL, FILTER인데… 왜 비었냐…

 

그 뒤는 INFO로 따로 또 딕셔너리로 묶여있는 공간이다. 그래서 이게 다 뭐냐고?
1. ALLELEID: 대립 유전자 아이디
2. CLNDISDB: 질병 데이터베이스 링크
3. CLNDN: 질병 이름
4. CLNHGVS: HGVS 명명법
5. CLNREVSTAT: 검토 신뢰도
6. CLNSIG: 임상적 유의성
7. CLNVC: 변이 타입
8. GENEINFO: 유전자 정보(이름:Entrez ID)
9. MC: 분자적 영향
10. ORIGIN: 변이의 기원(0: 특정되지 않음)


csv파일로 만들기

일단 저 clinvar가 주기적으로 업데이트되는 데이터베이스입니다. 뭐 하루에 하나씩 바뀌고 그런건 아니고 1~2주마다 바뀌는듯한데, 바뀔때마다 파일명이 같이 바뀌거든요.

vcf_path = 'data/clinvar_20260208.vcf'
f_name = re.search('clinvar_[0-9]{8}', vcf_path).group()

print(f'file name: {f_name}')

근데 저 이름 형식이 바뀌는건 아니라 큐식정(정규표현식)으로 패턴 잡아서 찾아서 이름 추출하면 장땡이긴 해요. 저렇게만 해두면 새 파일을 받았을 때 vcf_path 변수만 새 파일명으로 수정하면 된다.

 

# 1. VCF 읽기 (이전의 DtypeWarning 해결 버전)
def read_vcf_full(path):
    with open(path, 'r') as f:
        lines = [l for l in f if not l.startswith('##')]

    df = pd.read_csv(
        io.StringIO(''.join(lines)),
        sep='\t',
        dtype={'#CHROM': str},
        low_memory=False
    ).rename(columns={'#CHROM': 'CHROM'})

    # 2. INFO 컬럼을 딕셔너리로 파싱하는 함수
    def parse_info(info_str):
        # 'KEY=VALUE' 쌍들을 분리하여 딕셔너리 생성
        info_dict = {}
        for item in info_str.split(';'):
            if '=' in item:
                key, value = item.split('=', 1)
                info_dict[key] = value
            else:
                info_dict[item] = True  # 값이 없는 플래그(Flag) 처리
        return info_dict

    # 3. INFO 파싱 적용 및 데이터프레임 확장
    info_df = pd.DataFrame(df['INFO'].apply(parse_info).tolist())

    # 4. 기존 컬럼과 합치기 (INFO 원본은 삭제)
    final_df = pd.concat([df.drop(columns=['INFO']), info_df], axis=1)

    return final_df

# 실행
clinvar_df = read_vcf_full(vcf_path)

데이터프레임을 만드는 과정인데 이게 정말 개같이 오래 걸립니다. 돌려놓고 똥때리고 오십쇼. 왜 오래걸리냐면 쟤가 행만 한 400만개 됨..

 

clinvar_df.to_csv(f'data/{f_name}.csv', index=False)
print(f'saved: data/{f_name}.csv')

똥때리고 왔더니 데이터프레임이 나왔다... 그럼 csv로 저장하시면 됩니다. 참고로 이거 여는것도 오래 걸림. 

 

근데 태블로가 왜 거기서 나오냐고요? 사람 염색체가 25개입니다. 왜죠? 상염색체 22개+성염색체 두개(XY)+미토콘드리아(얘도 지 유전자가 따로 있음) 해서 25개다. 이걸 하나하나 따로 보는 코딩 자체도 빡세고 귀찮은데 노트북 길어지면 보겠어요? 한 3번염색체까지 보고 에이씨 이거 언제끝나 하겠지. 그리고 저걸 다 시각화하면 그래프가 25개 나오는데 배치도 배치지만 저정도면 그래프 그려도 제대로 안보여요.

 

그러니까 우리가 할 분석들 중에서 염색체에 대한 시각화를 태블로로 뺄거다. 거기서 대시보드 만들고 필터 만들어서 샥샥 하면 끝남.

반응형

'Coding > EDA' 카테고리의 다른 글

얘! clinvar도 EDA가 된단다! (3)  (0) 2026.02.18
얘! clinvar도 EDA가 된단다! (2)  (0) 2026.02.17
Ramen ratings  (0) 2026.02.11
Post-COVID Video Games Worldwide (2021-2025)  (0) 2026.02.10
또 ChEMBL을 털어보았다  (0) 2026.01.28
반응형

카테고리를 보고 이게 여기가 맞나 싶으셨죠? 맞습니다. 파이썬 코딩한거임.


그 공식은 뭔지 구글에 찾아보면 나오는데, 뭐 희석할때 농도 얼마 맞추려면 얼마나 넣어야되나 구할 때 쓰는 공식입니다. 근데 계산할때 단위는 맞추셔야 됩니다. 한쪽은 리터인데 한쪽은 밀리리터면 계산 뻑나요.

 

# M1V1 = M2V2
# 이거 되게 간단한 희석 농도 구하는 공식입니다. 
# 예를 들어서 100mM 염화나트륨 용액 xml를 넣어서 50mM 염화나트륨 100ml를 만들어야 해요. 그러면 100 * x = 50 * 100이 되거든요. 
# 그러면 100x = 5000이니까 100으로 나누면 x = 50이 됩니다. 
# 예시를 몰(M)로 들어서 글치 스톡 솔루션(농축액)에도 적용되는 공식입니다 이거. 

# 참고로 단위 통일하셔야 합니다. 하나는 리터 하나는 밀리리터 이렇게 하시면 계산 뻑나요. 

# V1 구하는 함수
def calculate_v1 (m1, m2, v2): 
    v1 = (m2 * v2) / m1
    return v1

# V2 구하는 함수 
# 근데 이게 필요함? 
def calculate_v2 (m1, v1, m2): 
    v2 = (m1 * v1) / m2
    return v2

# M1 구하는 함수
def calculate_m1 (v1, m2, v2):
    m1 = (m2 * v2) / v1
    return m1

# M2 구하는 함수
def calculate_m2 (m1, v1, m2): 
    m2 = (m1 * v1) / v2
    return m2

# 예시(v1)
# 5x stock solution으로 2x(2배 농도) 용액 500ml를 만들 때 필요한 부피는? (보통 나머지는 물로 채웁니다)
v1 = calculate_v1(5, 2, 500)
print(f'v1: {v1:.2f}')

# 예시(v2)
# 10x stock solution 100ml을 써서 2x 용액을 몇 ml 만들 수 있나요? 
v2 = calculate_v2(10, 100, 2)
print(f'v2: {v2:.2f}')

# 예시(m1)
# 농도를 모르는 stock solution을 300ml 넣어서 2x 용액 600ml를 만들었다면 원재료의 농도는? 
m1 = calculate_m1(300, 2, 600)
print(f'm1: {m1:.2f}')

# 예시(m2)
# 5x stoxk solution 100ml을 이용하여 만들 수 있는 500ml 용액의 농도는? 
m2 = calculate_m2(5, 100, 500)
print(f'm2: {m2:.2f}')

하나만 할까 하다가 4개 다했음.

반응형
반응형

https://www.kaggle.com/datasets/residentmario/ramen-ratings

 

Ramen Ratings

Over 2500 ramen ratings

www.kaggle.com

그... 돈코츠 이런거 아니고 우리 먹는 라면임다.


데이터 입수

import kagglehub

# Download latest version
path = kagglehub.dataset_download("residentmario/ramen-ratings")

print("Path to dataset files:", path)
ramen_df = pd.read_csv(f'{path}/ramen-ratings.csv')

우리는 지혜롭게 해결해야 합니다. 창고 원격으로 털어가라고 줬으면 걍 원격으로 털어갑시다.


전처리

결측값 처리

ramen_df['Style'] = ramen_df['Style'].fillna('Pack')
# 둘다 팩이래요

찾아보니까 둘다 봉지라면이라서 그거 채웠음. Top 10에 결측값인거요? 그거는 걍 두셈.

 

문자인 척 하는 놈 검거 

ramen_df['Stars'] = pd.to_numeric(ramen_df['Stars'], errors='coerce') # 별점
ramen_df['Review #'] = pd.to_numeric(ramen_df['Review #'], errors='coerce') # 리뷰 수

니네 숫자인데 왜 오브젝트냐고…

 

브랜드명 통일

ramen_df['Brand'] = ramen_df['Brand'].replace('Chorip Dong', 'ChoripDong')
ramen_df['Brand'] = ramen_df['Brand'].replace('Samyang Foods', 'Samyang')

얘들아… 브랜드좀 알아서 맞춰…

 

롸? 초립동? 저거 뭔 브랜드예요? 국내에서는 볼일이 잘 없는데, 본인이 재외동포거나 외국 여행갔다가 한인마트를 간 적 있다면 거기서 보셨을 것이다. 외국 한인마트에 들어가는 브랜드임.

 

줄바꿈이 왜 거기서 나와~ 

ramen_df['Top Ten'] = ramen_df['Top Ten'].replace('\n', np.nan, regex=True)

뭘 쓰려다가 만겁니까 용사여...


국가별 분석

국가별 라면 개수

ramen_df_count = ramen_df.groupby('Country')['Review #'].agg('count').sort_values(ascending = False).reset_index()
plt.figure(figsize = (18, 9))
ax = sns.barplot(ramen_df_count, x = 'Country', y = 'Review #', hue = 'Country', palette = 'Spectral')

# 라벨 박아줘야죠
for i in ax.patches:
    height = i.get_height()
    ax.annotate(f'{height:.1f}',  # 표시할 텍스트 (소수점 1자리)
                (i.get_x() + i.get_width() / 2., height), # 위치: 막대 중앙 상단
                ha='center', va='bottom', size=11) # 정렬 및 크기

plt.title('국가별 라면 개수', fontsize = 20)
plt.xlabel('국가')
plt.ylabel('라면 개수')
plt.xticks(rotation=90)
plt.show()

우리나라는 일본, 미국 다음으로 3위다.

 

별점 3점 이상인 라면

ramen_df_rating3 = ramen_df.query('Stars >= 3') # 별점 3점 이상
ramen_df_rating3 = ramen_df_rating3.groupby('Country')['Review #'].agg('count').sort_values(ascending = False).reset_index()

ramen_df_rating3['Review #']
plt.figure(figsize = (18, 9))
ax = sns.barplot(ramen_df_rating3, x = 'Country', y = 'Review #', hue = 'Country', palette = 'Spectral')

# 라벨 박아줘야죠
for i in ax.patches:
    height = i.get_height()
    ax.annotate(f'{height:.1f}',  # 표시할 텍스트 (소수점 1자리)
                (i.get_x() + i.get_width() / 2., height), # 위치: 막대 중앙 상단
                ha='center', va='bottom', size=11) # 정렬 및 크기

plt.title('국가별 라면 개수 (별점 3점 이상)', fontsize = 20)
plt.xlabel('국가')
plt.ylabel('라면 개수')
plt.xticks(rotation=90)
plt.show()

3점 이상인 라면은 미국보다 우리가 더 많음.

 

국가별 별점 평균

ramen_df_mean = ramen_df.groupby('Country')['Stars'].agg('mean').sort_values(ascending = False).reset_index()
ramen_df_mean
plt.figure(figsize = (18, 9))
ax = sns.barplot(ramen_df_mean, x = 'Country', y = 'Stars', hue = 'Country', palette = 'Spectral')

# 라벨 박아줘야죠
for i in ax.patches:
    height = i.get_height()
    ax.annotate(f'{height:.1f}',  # 표시할 텍스트 (소수점 1자리)
                (i.get_x() + i.get_width() / 2., height), # 위치: 막대 중앙 상단
                ha='center', va='bottom', size=11) # 정렬 및 크기

plt.xlabel('국가')
plt.ylabel('별점 평균')
plt.xticks(rotation=90)
plt.title('국가별 라면 별점 평균', fontsize = 20)
plt.show()

음... 브라질은 의외구만.

 

국가별로 별점이 제일 높은 라면

high_star_idx = ramen_df.dropna(subset=['Country', 'Stars']).groupby('Country')['Stars'].idxmax()
ramen_df.loc[high_star_idx].sort_values('Stars', ascending = False).reset_index()
	index	Review #	Brand	Variety	Style	Country	Stars	Top Ten
0	512	2068	Maggi	Fusian Special Edition Ow... Ow... Spicy Cow M...	Pack	Australia	5.00	NaN
1	251	2329	Patanjali	Atta Noodles Jhatpat Banao Befikr Khao	Pack	India	5.00	NaN
2	11	2569	Yamachan	Yokohama Tonkotsu Shoyu	Pack	USA	5.00	NaN
3	380	2200	Mr. Lee's Noodles	Shaolin Monk Vegetables	Cup	UK	5.00	NaN
4	10	2570	Tao Kae Noi	Creamy tom Yum Kung Flavour	Pack	Thailand	5.00	NaN
5	65	2515	Uni-President	Man Han Feast Spicy Beef Flavor Instant Noodles	Bowl	Taiwan	5.00	NaN
6	30	2550	Samyang	Paegaejang Ramen	Pack	South Korea	5.00	NaN
7	22	2558	KOKA	Creamy Soup With Crushed Noodles Hot & Sour Fi...	Cup	Singapore	5.00	NaN
8	883	1697	The Kitchen Food	Instant Kampua Dark Soy Sauce	Pack	Sarawak	5.00	NaN
9	2033	547	Lucky Me!	Pancit Canton Sweet Spicy	Pack	Philippines	5.00	NaN

?? 삼양에서 파개장 라면을 냈었음? 저 왜 못봤죠?


K-라면

k_ramen = ramen_df.query('Country == "South Korea"') # 니네 DB에 이북산 라면도 있니?
k_ramen

저거 어차피 한국 라면밖에 없음... 노스 없으니까 번잡시러우시면 Korea로 바꾸십셔.

 

브랜드별 라면 개수

k_ramen_cnt = k_ramen.groupby('Brand')['Stars'].agg('count').sort_values(ascending = False).reset_index()
plt.figure(figsize = (18, 9))
ax = sns.barplot(k_ramen_cnt, x = 'Brand', y = 'Stars', hue = 'Brand', palette = 'Spectral')

# 라벨 박아줘야죠
for i in ax.patches:
    height = i.get_height()
    ax.annotate(f'{height:.1f}',  # 표시할 텍스트 (소수점 1자리)
                (i.get_x() + i.get_width() / 2., height), # 위치: 막대 중앙 상단
                ha='center', va='bottom', size=11) # 정렬 및 크기

plt.title('브랜드별 K-라면 개수', fontsize = 20)
plt.xlabel('국가')
plt.ylabel('라면 개수')
plt.xticks(rotation=90)
plt.show()

삼양, 팔도, 농심, 오뚜기가 압도적이고 그 다음이 풀무원이다. 저 다섯개 브랜드 라면 함 까봐야징.

 

브랜드별 평균 별점

k_ramen_mean = k_ramen.groupby('Brand')['Stars'].agg('mean').sort_values(ascending = False).reset_index()
plt.figure(figsize = (18, 9))
ax = sns.barplot(k_ramen_mean, x = 'Brand', y = 'Stars', hue = 'Brand', palette = 'Spectral')

# 라벨 박아줘야죠
for i in ax.patches:
    height = i.get_height()
    ax.annotate(f'{height:.1f}',  # 표시할 텍스트 (소수점 1자리)
                (i.get_x() + i.get_width() / 2., height), # 위치: 막대 중앙 상단
                ha='center', va='bottom', size=11) # 정렬 및 크기

plt.title('국가별 라면 별점', fontsize = 20)
plt.xlabel('국가')
plt.ylabel('라면 개수')
plt.xticks(rotation=90)
plt.show()

그… 많이 판다고 별점까지 다 좋진 않아요…

 

개별 브랜드-팔도

paldo = k_ramen.query('Brand == "Paldo"')
paldo # 불닭 언제 나오나 본다 내가

불닭볶음면은 삼양이니까 한참 더 가셔야됩니다.

 

팔도의 5성급 라면

paldo_5_star = paldo.query('Stars >= 5')
paldo_5_star
	Review #	Brand	Variety	Style	Country	Stars	Top Ten
97	2483	Paldo	Bul Jjamppong	Bowl	South Korea	5.0	NaN
256	2324	Paldo	Bul Jjajangmyeon	Pack	South Korea	5.0	NaN
346	2234	Paldo	Bibim Men	Bowl	South Korea	5.0	NaN
360	2220	Paldo	Budae Jjigae	Pack	South Korea	5.0	NaN
826	1754	Paldo	King Bowl Super Spicy Pan Stirfried Noodle	Bowl	South Korea	5.0	NaN
903	1677	Paldo	Raobokki Noodle (Export Version)	Pack	South Korea	5.0	NaN
1005	1575	Paldo	Jjajangmen Chajang Noodle King Bowl	Bowl	South Korea	5.0	NaN
1057	1523	Paldo	Jjamppong Seafood Noodle King Bowl	Bowl	South Korea	5.0	NaN
1166	1414	Paldo	Cheese Ramyun (for US market)	Pack	South Korea	5.0	NaN
1266	1314	Paldo	Korean Traditional Beef Gomtangmen	Pack	South Korea	5.0	NaN
1397	1183	Paldo	Cheese Noodle	Pack	South Korea	5.0	2014 #6
1648	932	Paldo	Namja Ramen (USA version)	Pack	South Korea	5.0	NaN
1754	826	Paldo	Namja	Pack	South Korea	5.0	NaN
1756	824	Paldo	Bibim Men Cucumber	Pack	South Korea	5.0	NaN
1757	823	Paldo	Kokomen Spicy Chicken	Pack	South Korea	5.0	2013 #9
1906	674	Paldo	Kko Kko Myun	Pack	South Korea	5.0	NaN

어... 나도 꼬꼬면 참 좋아해... 좋아하는데... 이정도로 월클일 줄 몰랐어... 비빔면은 나는 매워서 못먹지만 솔직히 월클일만 했음.

 

도시락

target_ramens = paldo[paldo['Variety'].str.contains('Dosirac', case=False)]
print(target_ramens[['Brand', 'Variety', 'Stars']])
      Brand                        Variety  Stars
1579  Paldo  ДОШИРАК (Dosirac) Beef Flavor  3.500
2266  Paldo               Dosirac Mushroom  2.500
2267  Paldo                 Dosirac Shrimp  4.250
2271  Paldo                   Dosirac Beef  3.750
2277  Paldo     Dosirac Artificial Chicken  3.250
2404  Paldo                   Dosirac Pork  4.125

리뷰어 양반… 편의점에 김치도시락 있으니까 먹어보라우… 저게 그 어머니 러시아에서 히트라는 네모네모 라면입니다. 아 왕뚜껑이요? 나도 좋아해 좋아하는데 영어로 뭐라고 하는지 몰라…

 

개별 브랜드-농심

nongshim = k_ramen.query('Brand == "Nongshim"')
nongshim # 불닭 언제 나오나 본다 내가

내가 신라면은 매워서 못먹고… 새우탕면 맛있습니다. 백목이버섯 불려서 슬금슬금 넣어먹으면 아주 국물이 크… 그 백목이버섯은 국물이 약간 매콤한 라면이랑 어울려요. 진라면 약간매운맛이나 새우탕면같은… 나중에 오징어짬뽕으로도 테스트해보겠음.

 

농심 5성급

nongshim_5_star = nongshim.query('Stars >= 5')
nongshim_5_star
Review #	Brand	Variety	Style	Country	Stars	Top Ten
47	2533	Nongshim	Shin Ramyun Black	Pack	South Korea	5.0	NaN
419	2161	Nongshim	Chal Bibim Myun	Pack	South Korea	5.0	NaN
486	2094	Nongshim	Champong Noodle Soup Spicy Seafood Flavor	Pack	South Korea	5.0	NaN
753	1827	Nongshim	Zha Wang ((Jjawang) Noodles With Chajang Sauce	Pack	South Korea	5.0	NaN
979	1601	Nongshim	Jinjja Jinjja (New)	Pack	South Korea	5.0	NaN
1272	1308	Nongshim	Soon Veggie Noodle Soup	Pack	South Korea	5.0	2014 #9
1475	1105	Nongshim	Doong Ji Authentic Korean Cold Noodles With Ch...	Tray	South Korea	5.0	NaN
1829	751	Nongshim	Shin Ramyun Black Onion	Cup	South Korea	5.0	NaN
1835	745	Nongshim	Jinjja Jinjja	Pack	South Korea	5.0	NaN

그... 둥지냉며어어어어어언이요... 조낸 비싸요... 조낸 비싼데 조낸 간단해... 우리가 모밀이나 비빔면은 라면류가 많지만 냉면은 쟤 하나거든요? 아 그래서 비싸게 받는건가... 아무튼 이게 냉면인데 걍 라면 끓여먹듯 끓이면 되고 국물도 물타면 땡입니다. 조낸 비싼거 빼면 다 좋음. 집에 김치 있어요? 백김치건 동치미건 말아잡수면 최고임.

 

너구리

target_ramens = nongshim[nongshim['Variety'].str.contains('Neoguri', case=False)]
print(target_ramens[['Brand', 'Variety', 'Stars']])
         Brand                      Variety  Stars
1065  Nongshim  Neoguri Udon Seafood & Mild   4.00
1673  Nongshim        Neoguri Spicy Seafood   4.00
1771  Nongshim   Neoguri Mild (South Korea)   4.00
2079  Nongshim                 Neoguri Mild   3.25
2560  Nongshim    Neoguri (Seafood'n'Spicy)   3.50

너구리가… 한국판이랑 걍 너구리랑 뭔 차이임? 수출버전에는 다시마가 없어?

 

신라면

target_ramens = nongshim[nongshim['Variety'].str.contains('Shin', case=False)]
print(target_ramens[['Brand', 'Variety', 'Stars']])
         Brand                       Variety  Stars
47    Nongshim             Shin Ramyun Black   5.00
76    Nongshim                   Shin Ramyun   3.00
1393  Nongshim               Shin Ramyun Cup   3.50
1582  Nongshim  Shin Ramyun Black Spicy Beef   4.50
1829  Nongshim       Shin Ramyun Black Onion   5.00
2002  Nongshim             Shin Ramyun Black   4.75
2238  Nongshim                 Shin Big Bowl   3.50
2289  Nongshim                     Shin Bowl   3.00
2561  Nongshim                   Shin Ramyun   4.00

신라면 블랙이랑 블랙어년이 5점이다. 나는 저 라인은 다 매워서 못먹음...

 

안성탕면

target_ramens = nongshim[nongshim['Variety'].str.contains('Ansungtangmyun', case=False)]
print(target_ramens[['Brand', 'Variety', 'Stars']])
         Brand                     Variety  Stars
2558  Nongshim  Ansungtangmyun Noodle Soup   3.75

자네 빨리 와서 순하리랑 해물 안성탕면좀 먹고 가게.

 

개별 브랜드-삼양

samyang = k_ramen.query('Brand == "Samyang"')
samyang # 불닭 거기

불닭볶음면으로 킹이 된 삼양… 정확히는 삼양식품이요.

 

5성급 라면

samyang_5_star = samyang.query('Stars >= 5')
samyang_5_star
	Review #	Brand	Variety	Style	Country	Stars	Top Ten
30	2550	Samyang	Paegaejang Ramen	Pack	South Korea	5.0	NaN
69	2511	Samyang	Samyang Ramen Classic Edition	Bowl	South Korea	5.0	NaN
214	2366	Samyang	Buldak Bokkeummyun Snack	Pack	South Korea	5.0	NaN
215	2365	Samyang	Stew Buldak Bokkeumtangmyun	Pack	South Korea	5.0	NaN
298	2282	Samyang	Gold Jjamppong Fried Noodle	Pack	South Korea	5.0	NaN
606	1974	Samyang	Cheese Curry Ramyun	Pack	South Korea	5.0	NaN
1280	1300	Samyang	Red Nagasaki Jjampong	Pack	South Korea	5.0	NaN
1382	1198	Samyang	Maesaengyitangmyun Baked Noodle	Pack	South Korea	5.0	2014 #5
1551	1029	Samyang	Nagasaki Crab Jjampong	Pack	South Korea	5.0	NaN

아 불닭볶음면이 국물버전이 있어?

 

불닭볶음면 씨리즈

target_ramens = samyang[samyang['Variety'].str.contains('Buldak', case=False)]
print(target_ramens[['Brand', 'Variety', 'Stars']])
        Brand                                     Variety  Stars
72    Samyang                     Mala Buldak Bokkeummyun   3.75
99    Samyang                          Buldak Bokkeummyun   3.75
156   Samyang  Cheese Type Buldak Bokkeummyun (Black Pkg)   3.75
183   Samyang           Cheese Buldak Bokkeummyun (Black)   4.00
210   Samyang          Zzaldduck Buldak Bokkeummyun Snack   4.50
211   Samyang                    Curry Buldak Bokkeummyun   4.25
212   Samyang                 Cool/Ice Buldak Bokkeummyun   3.75
213   Samyang            2x Spicy Haek Buldak Bokkeummyun   4.00
214   Samyang                    Buldak Bokkeummyun Snack   5.00
215   Samyang                 Stew Buldak Bokkeumtangmyun   5.00
216   Samyang                   Cheese Buldak Bokkeummyun   4.00
217   Samyang          Buldak Bokkeummyun (New Packaging)   4.00
289   Samyang             Buldak Bokkummyun Cheese Flavor   4.00
1150  Samyang                          Buldak Bokkummyeon   4.00

불닭볶음면이 왜 두개니…

 

핵불닭은 별 4개다. ...당신들 다음날 내장은 괜찮은겁니까?

 

삼양라면

target_ramens = samyang[samyang['Variety'].str.contains('Samyang Ramen|Samyang Ramyun', case=False)]
print(target_ramens[['Brand', 'Variety', 'Stars']])
        Brand                                       Variety  Stars
69    Samyang                 Samyang Ramen Classic Edition   5.00
1330  Samyang  三養라면 (Samyang Ramyun) (South Korean Version)   3.75
1467  Samyang                   Samyang Ramyun (SK Version)   3.50
1557  Samyang                                Samyang Ramyun   4.50

저 클래식은 대체 뭘까… 예전에 그 투명포장에 삼양라-면 있고 닭어쩌고 하던 그건가?

 

개별 브랜드-오뚜기

ottogi = k_ramen.query('Brand == "Ottogi"')
ottogi

 

오뚜기의 5성급 라면

ottogi_5_star = ottogi.query('Stars >= 5')
ottogi_5_star
	Review #	Brand	Variety	Style	Country	Stars	Top Ten
189	2391	Ottogi	Jin Jjambbong Spicy Seafood Ramyun	Pack	South Korea	5.0	NaN

저거 굴진짬뽕도 맛있습니다. 개인적으로 흰 국물이라 나는 굴진짬뽕을 더 좋아함.

 

진라면

target_ramens = ottogi[ottogi['Variety'].str.contains('Jin Ramen', case=False)]
print(target_ramens[['Brand', 'Variety', 'Stars']])
       Brand                    Variety  Stars
1800  Ottogi  Jin Ramen (Mild) (Import)   3.50
1959  Ottogi   Jin Ramen Big Bowl (Hot)   3.50
2085  Ottogi           Jin Ramen (Mild)   3.25
2185  Ottogi           Jin Ramen (Mild)   3.00
2257  Ottogi            Jin Ramen (Hot)   3.50
2562  Ottogi      Jin Ramen (Hot Taste)   3.50

그... 외국에는 약간매운맛이 없음?

 

진순 vs 진매

jin_mild = ottogi[ottogi['Variety'].str.contains('Mild', case=False)]
jin_hot = ottogi[ottogi['Variety'].str.contains('Hot', case=False)]

mean_mild = np.mean(jin_mild['Stars'])
mean_hot = np.mean(jin_hot['Stars'])

print(f'진순이 별점: {mean_mild} | 진매 별점 {mean_hot}')
if mean_mild > mean_hot:
    print('진순이 만세!')
else:
    print('진순이 매니아는 웁니다. ')
진순이 별점: 3.25 | 진매 별점 3.4
진순이 매니아는 웁니다.

아니! 진순이가! 어때서! 

 

참깨라면

target_ramens = ottogi[ottogi['Variety'].str.contains('Sesame', case=False)]
print(target_ramens[['Brand', 'Variety', 'Stars']])
       Brand                                          Variety  Stars
930   Ottogi  Sesame Flavor Ramen Korean Style Instant Noodle   4.25
1572  Ottogi                        Sesame Flavor Noodle Bowl   3.50

어… 그렇구나…

 

오동통

target_ramens = ottogi[ottogi['Variety'].str.contains('Odongtong', case=False)]
print(target_ramens[['Brand', 'Variety', 'Stars']])
       Brand                      Variety  Stars
1845  Ottogi       Odongtong Myon Seafood   2.75
2469  Ottogi  Odongtongmyon Seafood Spicy   3.25

자네... 다시마 두개 들어간거 먹어본거지...?

 

뿌셔뿌셔는 스낵이여 이사람들아 

target_ramens = ottogi[ottogi['Variety'].str.contains('Ppushu', case=False)]
print(target_ramens[['Brand', 'Variety', 'Stars']])
       Brand                                          Variety  Stars
50    Ottogi         Ppushu Ppushu Noodle Snack Honey Butter    2.00
106   Ottogi  Ppushu Ppushu Noodle Snack Chilli Cheese Flavor   4.25
1162  Ottogi             Ppushu Ppushu Grilled Chicken Flavor   1.00
1302  Ottogi        Ppushu Ppushu Noodle Snack Bulgogi Flavor   3.25
2117  Ottogi                           Ppushu Ppushu Barbecue   3.00
2130  Ottogi                          Ppushu Ppushu Tteobokki   3.25
2341  Ottogi                       Ppushu Ppushu Sweet & Sour   1.75

이건 라면이 아니고 부셔먹으라고 나온 과자인데 왜 여기 있는거임?

 

개별 브랜드-풀무원

pulmuone = k_ramen.query('Brand == "Pulmuone"')
pulmuone # 불닭 옛저녁에 나옴

얘네가 라면이 있나 싶으실텐데 그 로스팅 시리즈 있습니다. 파기름 짜장 맛있음.

 

풀무원의 5성급

pulmuone_5_star = pulmuone.query('Stars >= 5')
pulmuone_5_star
	Review #	Brand	Variety	Style	Country	Stars	Top Ten
393	2187	Pulmuone	Non-Fried Ramyun Noodle (Crab Flavor)	Pack	South Korea	5.0	NaN

저거 로스팅 홍게짬뽕인가? 안먹어봤는데 기회가 된다면 백목이버섯 불려서 넣은거 먹어보고 싶음.

 

TOP 10 노미네이트

k_ramen.query('not `Top Ten`.isna()')
	Review #	Brand	Variety	Style	Country	Stars	Top Ten
1272	1308	Nongshim	Soon Veggie Noodle Soup	Pack	South Korea	5.00	2014 #9
1382	1198	Samyang	Maesaengyitangmyun Baked Noodle	Pack	South Korea	5.00	2014 #5
1397	1183	Paldo	Cheese Noodle	Pack	South Korea	5.00	2014 #6
1757	823	Paldo	Kokomen Spicy Chicken	Pack	South Korea	5.00	2013 #9
2002	578	Nongshim	Shin Ramyun Black	Pack	South Korea	4.75	2012 #7

오오 꼬꼬면 오오


TOP 10

가장 많이 입성한 국가

top10_nominated = ramen_df.query('not `Top Ten`.isna()')
top10_nominated_cnt = top10_nominated.groupby('Country')['Variety'].agg('count').sort_values(ascending = False).reset_index()
plt.figure(figsize = (18, 9))
ax = sns.barplot(top10_nominated_cnt, x = 'Country', y = 'Variety', hue = 'Country', palette = 'Spectral')

# 라벨 박아줘야죠
for i in ax.patches:
    height = i.get_height()
    ax.annotate(f'{height:.1f}',  # 표시할 텍스트 (소수점 1자리)
                (i.get_x() + i.get_width() / 2., height), # 위치: 막대 중앙 상단
                ha='center', va='bottom', size=11) # 정렬 및 크기

plt.title('국가별 Top 10에 입성한 개수', fontsize = 20)
plt.xlabel('국가')
plt.ylabel('입성한 라면')
plt.xticks(rotation=90)
plt.show()

싱가포르 라면 궁금하구만. 말레이시아는 숨겨진 라면계의 강자라고 합니다.

 

TOP 10 별점 평균

top10_nominated_mean = top10_nominated.groupby('Country')['Stars'].agg('mean').sort_values(ascending = False).reset_index()
plt.figure(figsize = (18, 9))
ax = sns.barplot(top10_nominated_mean, x = 'Country', y = 'Stars', hue = 'Country', palette = 'Spectral')

# 라벨 박아줘야죠
for i in ax.patches:
    height = i.get_height()
    ax.annotate(f'{height:.2f}',  # 표시할 텍스트 (소수점 1자리)
                (i.get_x() + i.get_width() / 2., height), # 위치: 막대 중앙 상단
                ha='center', va='bottom', size=11) # 정렬 및 크기

plt.title('국가별 Top 10에 입성한 라면들의 별점 평균', fontsize = 20)
plt.xlabel('국가')
plt.ylabel('입성한 라면들의 별점 평균')
plt.xticks(rotation=90)
plt.show()

우리나라 라면은 아쉽게도 근소하게 빗나갔음…

 

별점 1점인 라면

ramen_df.query("Stars < 1")

거 클로렐라면은 대체…

 

ramen_under_1 = ramen_df.query("Stars < 1").groupby('Country')['Stars'].agg('count').sort_values(ascending=False).reset_index()
plt.figure(figsize = (18, 9))
ax = sns.barplot(ramen_under_1, x = 'Country', y = 'Stars', hue = 'Country', palette = 'Spectral')

# 라벨 박아줘야죠
for i in ax.patches:
    height = i.get_height()
    ax.annotate(f'{height:.1f}',  # 표시할 텍스트 (소수점 1자리)
                (i.get_x() + i.get_width() / 2., height), # 위치: 막대 중앙 상단
                ha='center', va='bottom', size=11) # 정렬 및 크기

plt.title('별점 1점 미만인 라면들의 국가 분포', fontsize = 20)
plt.xlabel('국가')
plt.ylabel('1점 미만인 라면 개수')
plt.xticks(rotation=90)
plt.show()

중국이랑 미국이 공동 1위다.


컵라면 vs 봉지라면

style_cup = ['Cup', 'Bowl']
style_pack = ['Pack']

ramen_cup = ramen_df.query('Style in @style_cup')
ramen_pack = ramen_df.query('Style in @style_pack')

컵이랑 볼은 왜 나누는겨…

 

5점대&1점 미만 비율

# 컵라면(Cup, Bowl) 비율 계산
cup_counts = ramen_cup['Stars'].value_counts(normalize=True)
cup_5 = cup_counts.get(5, 0) * 100
cup_under_1 = cup_counts[cup_counts.index < 1].sum() * 100

# 봉지라면(Pack) 비율 계산
pack_counts = ramen_pack['Stars'].value_counts(normalize=True)
pack_5 = pack_counts.get(5, 0) * 100
pack_under_1 = pack_counts[pack_counts.index < 1].sum() * 100

# 결과 출력
print(f"[컵라면] 5점 비율: {cup_5:.2f}%, 1점 미만 비율: {cup_under_1:.2f}%")
print(f"[봉지라면] 5점 비율: {pack_5:.2f}%, 1점 미만 비율: {pack_under_1:.2f}%")
[컵라면] 5점 비율: 13.86%, 1점 미만 비율: 2.79%
[봉지라면] 5점 비율: 15.62%, 1점 미만 비율: 1.63%

음... 쪽수도 쪽수인데... 전체적으로 컵라면이 호평받기가 빡신가배...

 

5점짜리 국가 분포

cup_5_cnt = cup_5star.groupby('Country')['Stars'].agg('count').sort_values(ascending=False).reset_index() # 컵
pack_5_cnt = pack_5star.groupby('Country')['Stars'].agg('count').sort_values(ascending=False).reset_index() # 팩
fig, ax = plt.subplots(1,2, figsize=(20, 10))

ax[0] = sns.barplot(cup_5_cnt, x = 'Country', y = 'Stars', hue = 'Country', palette = 'Spectral', ax=ax[0], legend=False)
ax[0].set_title('컵라면 별점 5점짜리 국가 분포', fontsize = 20)
ax[0].set_xlabel('국가')
ax[0].set_ylabel('개수')
ax[0].tick_params(axis='x', rotation=90)

ax[1] = sns.barplot(pack_5_cnt, x = 'Country', y = 'Stars', hue = 'Country', palette = 'Spectral', ax=ax[1], legend=False)
ax[1].set_title('봉지라면 별점 5점짜리 국가 분포', fontsize = 20)
ax[1].set_xlabel('국가')
ax[1].set_ylabel('개수')
ax[1].tick_params(axis='x', rotation=90)

# 레이아웃 조정 후 출력
plt.tight_layout()
plt.show()

일본이 컵라면 본좌긴 하지...

 

1점 미만 국가 분포

cup_1_cnt = cup_1star.groupby('Country')['Stars'].agg('count').sort_values(ascending=False).reset_index() # 컵
pack_1_cnt = pack_1star.groupby('Country')['Stars'].agg('count').sort_values(ascending=False).reset_index() # 팩
fig, ax = plt.subplots(1,2, figsize=(20, 10))

ax[0] = sns.barplot(cup_1_cnt, x = 'Country', y = 'Stars', hue = 'Country', palette = 'Spectral', ax=ax[0], legend=False)
ax[0].set_title('컵라면 별점 1점짜리 국가 분포', fontsize = 20)
ax[0].set_xlabel('국가')
ax[0].set_ylabel('개수')
ax[0].tick_params(axis='x', rotation=90)

ax[1] = sns.barplot(pack_1_cnt, x = 'Country', y = 'Stars', hue = 'Country', palette = 'Spectral', ax=ax[1], legend=False)
ax[1].set_title('봉지라면 별점 1점짜리 국가 분포', fontsize = 20)
ax[1].set_xlabel('국가')
ax[1].set_ylabel('개수')
ax[1].tick_params(axis='x', rotation=90)

# 레이아웃 조정 후 출력
plt.tight_layout()
plt.show()

??? 컵라면은 왜 캐나다가 1위여? 라면에 메이플시럽 넣었냐 니네?

 

K-라면의 비중

# 국가가 사우쓰 코리아냐 아니냐로 구별
# 람다식 쓰시져
ramen_cup_kor = ramen_cup.copy()
ramen_cup_kor['Country'] = ramen_cup['Country'].apply(lambda x:"K-ramyeon" if x == "South Korea" else "Other")

ramen_pack_kor = ramen_pack.copy()
ramen_pack_kor['Country'] = ramen_pack['Country'].apply(lambda x:"K-ramyeon" if x == "South Korea" else "Other")
# 1. 데이터 집계 (Cup & Pack 각각)
cup_counts = ramen_cup_kor['Country'].value_counts()
pack_counts = ramen_pack_kor['Country'].value_counts()

# 2. 파이차트 그리기 (1행 2열)
fig, ax = plt.subplots(1, 2, figsize=(14, 7))

# 공통 스타일 설정
colors = ['#ff9999', '#ffc000'] # K-ramyeon은 눈에 띄게!
explode = [0.1, 0] # K-ramyeon 살짝 튀어나오게

# 컵라면 파이차트
ax[0].pie(cup_counts, labels=cup_counts.index, autopct='%1.1f%%',
        startangle=90, colors=colors, explode=explode, shadow=True)
ax[0].set_title('컵라면: 한국 vs 타국가 비율', fontsize=16)

# 봉지라면 파이차트
ax[1].pie(pack_counts, labels=pack_counts.index, autopct='%1.1f%%',
        startangle=90, colors=colors, explode=explode, shadow=True)
ax[1].set_title('봉지라면: 한국 vs 타국가 비율', fontsize=16)

plt.tight_layout()
plt.show()

봉지라면의 비율이 쬐끔 더 높다.

 

TOP 10의 비중

cup_top10 = ramen_cup.copy()
pack_top10 = ramen_pack.copy()
cup_top10['Is Top Ten'] = cup_top10['Top Ten'].apply(lambda x: 'Top Ten' if pd.notnull(x) and x != '' else 'None')
pack_top10['Is Top Ten'] = pack_top10['Top Ten'].apply(lambda x: 'Top Ten' if pd.notnull(x) and x != '' else 'None')
# 1. 데이터 집계 (Cup & Pack 각각)
cup_counts = cup_top10['Is Top Ten'].value_counts()
pack_counts = pack_top10['Is Top Ten'].value_counts()

# 2. 파이차트 그리기 (1행 2열)
fig, ax = plt.subplots(1, 2, figsize=(14, 7))

# 공통 스타일 설정
colors = ['#ff9999', '#ffc000'] # K-ramyeon은 눈에 띄게!
explode = [0.1, 0] # K-ramyeon 살짝 튀어나오게

# 컵라면 파이차트
ax[0].pie(cup_counts, labels=cup_counts.index, autopct='%1.1f%%',
        startangle=90, colors=colors, shadow=True)
ax[0].set_title('컵라면: TOP 10 비율', fontsize=16)

# 봉지라면 파이차트
ax[1].pie(pack_counts, labels=pack_counts.index, autopct='%1.1f%%',
        startangle=90, colors=colors, explode=explode, shadow=True)
ax[1].set_title('봉지라면: TOP 10 비율', fontsize=16)

plt.tight_layout()
plt.show()

컵라면은 없고요...

 

# 컵라면 중 Top Ten에 선정된 녀석들만 추출
pack_top10_winners = pack_top10.query('`Is Top Ten` == "Top Ten"')

# 그 안에서 국가별 비중 확인
pack_top10_korea = pack_top10_winners['Country'].value_counts()
print("Top 10에 선정된 봉지라면들의 국적 분포:")
print(pack_top10_korea)
# 컵라면 중 Top Ten에 선정된 제품의 브랜드와 제품명 출력
elite_pack = pack_top10.query('`Is Top Ten` == "Top Ten" and Country == "South Korea"')[['Brand', 'Variety', 'Country', 'Top Ten']]

print("--- 봉지라면계의 전설(들) ---")
print(elite_pack)
--- 봉지라면계의 전설(들) ---
         Brand                          Variety      Country  Top Ten
1272  Nongshim          Soon Veggie Noodle Soup  South Korea  2014 #9
1382   Samyang  Maesaengyitangmyun Baked Noodle  South Korea  2014 #5
1397     Paldo                    Cheese Noodle  South Korea  2014 #6
1757     Paldo            Kokomen Spicy Chicken  South Korea  2013 #9
2002  Nongshim                Shin Ramyun Black  South Korea  2012 #7

내 꼬꼬면은 남격때부터 챙겨먹었다만... 이정도일 줄 몰랐고...

반응형

Profile

Lv. 36 라이츄

광고 매크로 없는 청정한 블로그를 위해 노력중입니다. 근데 나만 노력하는 것 같음… ㅡㅡ