(270)

M1V1 = M2V2

카테고리를 보고 이게 여기가 맞나 싶으셨죠? 맞습니다. 파이썬 코딩한거임.그 공식은 뭔지 구글에 찾아보면 나오는데, 뭐 희석할때 농도 얼마 맞추려면 얼마나 넣어야되나 구할 때 쓰는 공식입니다. 근데 계산할때 단위는 맞추셔야 됩니다. 한쪽은 리터인데 한쪽은 밀리리터면 계산 뻑나요. # M1V1 = M2V2# 이거 되게 간단한 희석 농도 구하는 공식입니다. # 예를 들어서 100mM 염화나트륨 용액 xml를 넣어서 50mM 염화나트륨 100ml를 만들어야 해요. 그러면 100 * x = 50 * 100이 되거든요. # 그러면 100x = 5000이니까 100으로 나누면 x = 50이 됩니다. # 예시를 몰(M)로 들어서 글치 스톡 솔루션(농축액)에도 적용되는 공식입니다 이거. # 참고로 단위 통일하셔야 합니..

Ramen ratings

https://www.kaggle.com/datasets/residentmario/ramen-ratings Ramen RatingsOver 2500 ramen ratingswww.kaggle.com그... 돈코츠 이런거 아니고 우리 먹는 라면임다.데이터 입수import kagglehub# Download latest versionpath = kagglehub.dataset_download("residentmario/ramen-ratings")print("Path to dataset files:", path)ramen_df = pd.read_csv(f'{path}/ramen-ratings.csv')우리는 지혜롭게 해결해야 합니다. 창고 원격으로 털어가라고 줬으면 걍 원격으로 털어갑시다.전처리결측값 처리ra..

Post-COVID Video Games Worldwide (2021-2025)

https://www.kaggle.com/datasets/otegbolamarvellous/post-covid-video-games-worldwide-2021-2025이거 했음.전처리결측값 처리meta_df_na = meta_df.query('User_Score.isna()').indexmeta_df.loc[meta_df_na]결측값 있는 칼럼이 저기말고 없는데, 확인해보니까 그 뭐라고 해야 되지? 리뷰가 너무 적어서 평점을 모을 수 없는? 그런 게임들이었음. 그런건 tbd라고 하는데 저기다가 그거 때려박으면 평점이 문자가 돼서 문제가 터져요. meta_df['User_Score'] = pd.to_numeric(meta_df['User_Score'], errors='coerce')그래서 이렇게만 함. 결..

코로나바이러스 MSA

나도 이걸 4번까지 하게 될 줄은 몰랐음…개요여러분들 다들 아시죠? 코로나19가 우리를 어떻게 변화시켰는지... 저는 자가격리도 해보고 걸려도 봤습니다. 자가격리때 다들 헐 어카냐 했는데… 저는 태생이 집순이라 1도 데미지 없었고요.. 격리소가 을지로 근처라 원격으로 명동성당 루기아 줘팼습니다. 창고는 언제나 열려있소# 쿼리 조건: SARS-CoV-2 (코로나19), Spike 단백질 위주로 털어보기# 2025년 최신 데이터 + 사람 숙주 조건query = "SARS-CoV-2 AND S[Gene Name] AND 2025[PDAT] AND Homo sapiens[Host]"# 1. ID 리스트 가져오기handle = Entrez.esearch(db="nucleotide", term=query, retm..

씨본 컬러 파레트 씨뮬레이터

그 Seaborn에서 컬러 커마 가능한거 아시죠? 근데 이게 뭔 색인지 뭔 파레트가 어떻게 나오는지 모르잖아요. ㅇㅋ? ㅇㅇㅋ. 그래서 색 두 개를 입력받은 다음 한 10칸정도로 띄엄띄엄 칠한거 하나, cmap(이어지는거)으로 하나 짜잔 하고 보여주자 이거다. 이거 근데 팀플에서도 써먹으려면 코드펜에도 올려야 할 듯. Seaborn palette simulator 그... 저기 인풋창에 색깔 두개 입력하시면 파레트랑 cmap이랑 보여줄거긴 합니다. 근데... 아... 세개 이상... 그거는 제 능력 밖이니까 걍 두개씩 돌려보세요... 이봐요 ..

또 ChEMBL을 털어보았다

오늘은 좀 다채롭습니다. 왜냐고? 뭐가 많이 들어서.그 우리 이제 할만큼 하셨으니까... 전처리는 다 안올릴게여... 내가 귀찮아서 이러는거 맞음. 내가 네이버 티스토리 미디움에 동시에 올리는데 그러면 복붙을 몇 번 하는겁니까...배경설명이번에 가져온 데이터베이스의 주제가 Kinase Inhibitor입니다. 이게 뭔데요? 를 설명하려면 kinase와 inhibitor를 다 설명해야 하는데… 아… Kinase는 뭐에 인산기를 붙이는 효소입니다. 인산기는 PO4(3-)고, 그걸 어디다가 붙이는 놈들이 다 카이네이스예요. 인산기를 어디다가 붙이는가는 효바효(효소 바이 효소)지만 어쨌든 인산기를 갖다 붙입니다. 보통 ATP에서 떼다가 붙이긴 합니다만. 자, 그럼 인히비터에 대한 얘기를 해봅시다. 우리 몸의 모..

라이노바이러스 유전자로 MSA를 해보았다

얘는 스케일이 좀 다운됐음... 왜냐고요? 게놈이 7500bp거든요. 이걸 인플루엔자나 한타때처럼 2~300개 돌린다? 켜놓고 자고 일어나야됩니다. 아니 리눅스로 하셨어요? 걔로 하면 중간에 뻗음. 맥북으로 돌린건데도 이정돕니다.쟤는 또 뭐 하는 애임?여러분 감기랑 독감이랑 다릅니다. 단순히 증상이 다른게 아니라 원인 병원체가 달라요. 독감은 인플루엔자가 원인이고 감기는 라이노바이러스라는 놈이 원인이거든요? 다른 바이러스도 있다만. 그거 아십니까? 감기에는 약이 없음. 아니 저희 병원가면 약 주는데요? 그건 '증상을 완화시키는' 약이지 감기 바이러스를 조지는 약이 아닙니다. 아니 그럼 감기약이라고 하면 안되는거 아닌가요? 진정하십쇼. 감기 바이러스는 스포닝풀에서 저글링 뽑아내는것처럼 캐많아요. 그걸 일일..

식물 데이터도 분석이 되나요?

결론부터 말하자면 분석은 된다. 되는데 데이터 리소스를 NCBI로 하는 건 비추다. 왜냐고? 그 이유는 올리면서 말해드림..def plot_lht_family_tree(alignment_file): align = AlignIO.read(alignment_file, "fasta") calculator = DistanceCalculator('identity') constructor = DistanceTreeConstructor(calculator, 'nj') tree = constructor.build_tree(align) fig = plt.figure(figsize=(15, 8)) ax = fig.add_subplot(1, 1, 1) plt.title("Arab..

포켓몬과 이항분포

https://koreanraichu.tistory.com/760 포켓몬 이로치가 나올 확률로 이항분포를 때려보자일단 이항분포가 뭐냐… 특정 확률(p)을 가진 베르누이 시행을 n번 독립적으로 반복했을 때, 성공하는 횟수(X)에 대한 이산 확률 분포라고 한다. Pass or Fail 뭐 이런건데, 여기서 중요한 건 결과가koreanraichu.tistory.com이거 파이썬으로 하는거 맞음.성비와 이항분포포켓몬에는 성비가 존재한다. 성비가 불명(무성)이거나 한쪽으로 쏠려있는 경우도 있지만 보통은 암수가 다 있는데 비율이 다른 경우가 많다. 님들 솔직히 세꿀버리 비퀸 만들어야되는데 암컷 안떠서 피눈물 흘리신 적 있으시죠? 난 있음... 아오 포고 야도뇽이 자꾸 수컷만 나와서 대환장파티여... # p(성비):..

캐글 EDA-마! 서퍼티파이! (2)

https://koreanraichu.tistory.com/839 캐글 EDA-마! 서퍼티파이!https://www.kaggle.com/datasets/serkantysz/550k-spotify-songs-audio-lyrics-and-genres/data 550K Spotify Songs: Audio, Lyrics & GenresEnhanced Music Dataset with Audio Features, Lyrics, Genres & Artist Metadatawww.kaggle.com참고로 본인은 스포티파koreanraichu.tistory.com우리 어제 전처리까지 하고 끝냈음… 기억하시죠? 하다하다 VScode가 뻗었다고…아티스트 분석여기는 뭐 없어서 분량도 짧다. # 1그룹에 다 몰렸구나.....

캐글 EDA-마! 서퍼티파이!

https://www.kaggle.com/datasets/serkantysz/550k-spotify-songs-audio-lyrics-and-genres/data 550K Spotify Songs: Audio, Lyrics & GenresEnhanced Music Dataset with Audio Features, Lyrics, Genres & Artist Metadatawww.kaggle.com참고로 본인은 스포티파이 계정이 있습니다. 왜냐고? 포슬립 사운드트랙이 거기 있으니까...import kagglehub# Download latest versionpath = kagglehub.dataset_download("serkantysz/550k-spotify-songs-audio-lyrics-and-ge..

그냥 해보는 ChEMBL EDA

이걸 근데 카테고리를 만들어야될지는 모르겠음… 이걸 매일 할 것 같지는 않고… 켐블서 특정 분자나 질환 치면 관련 화합물이 쭈루룩 나옵니다. 그거갖고 한거임.거 정보좀 봅시다df.shape()(63, 29)켐블 데이터 특: 칼럼 엄청 많음 df.info()RangeIndex: 63 entries, 0 to 62Data columns (total 29 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 ChEMBL ID 63 non-null object 1 Name 62 non-null object ..

캐글 EDA-Video game sales

https://www.kaggle.com/datasets/gregorut/videogamesales Video Game SalesAnalyze sales data from more than 16,500 games.www.kaggle.com본인은 겜덕후다. 자타공인 겜덕후이고 모바일 피씨 콘솔 할 것 없이 어지간한 게임은 다 했으며 가끔 무지하게 주관적인 리뷰를 올리기도 하고 집에는 각종 노랑뚱띵이와 굿즈들이 가득한. 그런데 비디오 게임 판매량 데이터??? 야이씨 이건 겜덕후로써 못참지! 해서 제미나이를 부려먹었다.모듈import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport seaborn as sns# 그래프 기본 테마 설정sns..

데이터프레임의 정보를 확인하는 몇 가지 방법

여러분은 데이터를 분석할 때 제일 먼저 해야 하는 게 뭔지 아십니까? 물론 데이터를 얻는걸 제일 먼저 해야겠지만, 데이터를 다 얻고 나면 데이터를 체크하고 전처리로 뭘 할 지를 정해야 한다. 근데 지금 리눅스 업글중이라 둘 다 다룰 수 있을지는 모르겠음.df.info()데이터프레임의 정보를 확인할 수 있다. RangeIndex: 299 entries, 0 to 298Data columns (total 13 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 age 299 non-null float..

M1V1 = M2V2

Coding/Python 2026. 2. 15. 02:25
반응형

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


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

 

# 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개 다했음.

반응형
Lv. 36 라이츄

Lv. 36 라이츄

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

Ramen ratings

Coding/EDA 2026. 2. 11. 21:00
반응형

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

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

반응형
Lv. 36 라이츄

Lv. 36 라이츄

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

Post-COVID Video Games Worldwide (2021-2025)

Coding/EDA 2026. 2. 10. 21:00
반응형

https://www.kaggle.com/datasets/otegbolamarvellous/post-covid-video-games-worldwide-2021-2025

이거 했음.


전처리

결측값 처리

meta_df_na = meta_df.query('User_Score.isna()').index
meta_df.loc[meta_df_na]

결측값 있는 칼럼이 저기말고 없는데, 확인해보니까 그 뭐라고 해야 되지? 리뷰가 너무 적어서 평점을 모을 수 없는? 그런 게임들이었음. 그런건 tbd라고 하는데 저기다가 그거 때려박으면 평점이 문자가 돼서 문제가 터져요.

 

meta_df['User_Score'] = pd.to_numeric(meta_df['User_Score'], errors='coerce')

그래서 이렇게만 함. 결측값은 계산할때 뺄 예정임.

 

플랫폼 개수로 범주화

# 1. 콤마(,)로 나누고(split), 양쪽 공백을 제거(strip)한 리스트의 길이를 측정
meta_df['Platform_classification'] = meta_df['Platform'].apply(
    lambda x: 'Singleplatform' if len(str(x).split(',')) == 1 else 'Multiplatform'
)

# 결과 확인
print(meta_df['Platform_classification'].value_counts())

게임 발매 플랫폼이 하나인거, 여러개인걸로 나눴다.

 

발매년도

# 일단 datetime으로 변환부터 합시다
meta_df['Release_Date'] = pd.to_datetime(meta_df['Release_Date'], dayfirst=True, errors='coerce')
meta_df['Year'] = meta_df['Release_Date'].dt.year # 응 년도 만들거야

발매일을 날짜로 변환한 다음 발매년도만 따로 뺐다. 저게 날짜 형식이 DD/MM/YYYY로 되어있어서 dayfirst 준 거임.

 

평점 범주화

def metascore_classificaion(score):
    if score >= 90:
        return 'Universal acclaim'
    elif score >= 75:
        return 'Generally favorable reviews'
    elif score >= 50:
        return 'Mixed or average reviews'
    elif score >= 20:
        return 'Generally unfavorable reviews'
    else:
        return 'Overwhelming dislike'

def userscore_classificaion(score):
    if score >= 9:
        return 'Universal acclaim'
    elif score >= 7.5:
        return 'Generally favorable reviews'
    elif score >= 5:
        return 'Mixed or average reviews'
    elif score >= 2:
        return 'Generally unfavorable reviews'
    else:
        return 'Overwhelming dislike'

그 메타크리틱 평점이 전문가랑 유저랑 나뉜거 아시죠? 유저 평점은 0~10이고 전문가 평점은 0~100입니다. 기준은 같은데 커트라인이 10배 줄어요 유저 점수가. 기준은 검색해서 매겼음.

 

meta_df['Metascore_classification'] = meta_df['Metascore'].apply(metascore_classificaion)
meta_df['Userscore_classification'] = meta_df['User_Score'].apply(userscore_classificaion)

근데 이거 만들어놓고 범주로는 한번도 뭐 한 게 없음…ㅋㅋㅋㅋ


본게임은 지금부터다

갓겜과 똥겜

평론가 평점이 90점 이상인 게임

# 일단 메타크리틱 평점이 90점 이상인 것만 따로 뻅시다.
meta_god_df = meta_df.query('Metascore >= 90')

meta_god_df

저기 엘든링 있던데? 

 

유저 평점도 9점 이상인 게임

# 평론가와 유저가 다 만족한 게임이 있을까?
meta_god_df.query('User_Score >= 9')

대체 발더스게이트는 얼마나 갓갓겜인거임???

 

유저 평점은 9점 미만인 게임

# 평론가와 유저 반응이 다른 게임
meta_god_df.query('User_Score < 9')

저기서 해본건 데이브 더 다이버 말고 없는듯... 데더다도 사냥하는건 재밌는데 문제가요... 초밥 운영이... 여기 초밥 먹으려고 며칠 굶고오나 손님들 성질이 조낸 급함...

 

평론가 평점이 20점 미만인 게임

# DDONG게임
meta_ddong_df = meta_df.query('Metascore < 20')

meta_ddong_df

일단 평론가 평점이 20점 미만인게 없음… 제일 낮은게 31점입니다. 변수명에 똥은 뭐냐고요? 아무리 그래도 shit을 변수명으로 쓸 순 없잖아… 그래서 ddong쓴거임.

 

유저 평점이 2점 미만인 게임

meta_ddong_df = meta_df.query('User_Score < 2')

meta_ddong_df

콩코드 유명하지... 지들이 왜 개쳐망했는지는 알까?

 

전문가 평점 50점 미만+유저 평점 2점 미만

# 50점 미만은 꽤 있다.
meta_ddong_df = meta_df.query('Metascore < 50')

meta_ddong_df
meta_ddong_df.query('User_Score < 2')

이게 전문가 평점이랑 유저 평점이랑 엇갈리는 경우도 있고 아닌경우도 있긴 있습니다. 근데 여기서는 공통분모가 없음.

 

플랫폼이 하나인가? 여러개인가?+PC에서도 발매됐는가?

meta_df.groupby(['Platform_classification','PC_included']).size()
Platform_classification  PC_included  
Multiplatform            PC_included      1579
                         PC_unincluded      71
Singleplatform           PC_included       179
                         PC_unincluded     177
dtype: int64

멀티플랫폼인 경우 대부분 PC도 포함한다. 그리고 플랫폼이 하나인 경우에는 PC판만 내거나, 콘솔 독점이거나… 요즘 근데 닌텐도 말고는 독점으로 잘 안 내지 않나?

 

sns.countplot(data=meta_df, x='Platform_classification', hue='PC_included', palette="icefire")
plt.legend(['PC판 미발매', 'PC판 발매'])
plt.xlabel('단일 플랫폼 여부 및 PC판 발매 여부')
plt.ylabel('발매 게임 수')
plt.title('단일 플랫폼 여부 및 PC판 발매여부에 따른 게임 수', fontsize=20)
plt.show()

요즘 스팀이나 에픽에서도 뭐 많이 할 수 있으니께... 게임패드요? PC에 연결 되는것도 많다. 가끔 스팀 맞춤 대기열 보면 원래 모바일게임으로 나왔던 게 갑자기 스팀으로 나오는 경우도 있다. 명조도 스팀으로 나왔고.

 

meta_df.groupby(['Year','Platform_classification','PC_included']).size()
Year  Platform_classification  PC_included  
2021  Multiplatform            PC_included      334
                               PC_unincluded     18
      Singleplatform           PC_included       20
                               PC_unincluded     36
2022  Multiplatform            PC_included      366
                               PC_unincluded     17
      Singleplatform           PC_included       41
                               PC_unincluded     37
2023  Multiplatform            PC_included      382
                               PC_unincluded     23
      Singleplatform           PC_included       26
                               PC_unincluded     49
2024  Multiplatform            PC_included      359
                               PC_unincluded     10
      Singleplatform           PC_included       62
                               PC_unincluded     34
2025  Multiplatform            PC_included      138
                               PC_unincluded      3
      Singleplatform           PC_included       30
                               PC_unincluded     21
dtype: int64

년도별로는 이렇게 된다. 멀티플랫폼인 게임은 23년도에, 싱글플랫폼인 게임은 24년도에 많이 발매됐다.

 

닌텐도도 멀티플랫폼이 있나요?

결론부터 말하자면 있긴 있다. 근데 자사 퍼스트파티나 세컨드파티가 멀티플랫폼으로 나오는 건 아니고... 얘네들 그런쪽으로 문호개방 안함... 대신 다른 게임사에서 저희 스위치로도 게임 내고 싶습니다! 하면 그건 웰컴이다. 가끔 모바일로 나오는건 중국산 짝퉁게임이거나 공식에서 내는 외전이거나.

nintendo_df = meta_df.query("Publisher.str.contains('Nintendo')", engine='python')
nintendo_df

어우 진여생 어우

 

sns.countplot(nintendo_df, x = 'Platform_classification', hue='Platform_classification', palette="icefire", legend=True)
plt.title('닌텐도도 멀티플랫폼이 있나요?', fontsize=20)
plt.xlabel('멀티플랫폼 여부')
plt.ylabel('발매 게임 수')
plt.legend()
plt.show()

퍼블리셔가 닌텐도면서 멀티플랫폼인 건 찐 극소수다.

 

년도별 갓겜/똥겜

년도별 갓겜

meta_god_loc = meta_df.groupby('Year')['Metascore'].idxmax()
meta_df.loc[meta_god_loc][['Title', 'Metascore', 'User_Score']].reset_index(drop=True)

아스트로봇 유명하데… 난 안해봤지만.

 

meta_god_loc = meta_df.groupby('Year')['User_Score'].idxmax()
meta_df.loc[meta_god_loc][['Title', 'Metascore', 'User_Score']].reset_index(drop=True)

공통분모 발더스게이트.. 대체 이 게임은 어떤 게임이길래 평론가와 유저들이 갓겜을 연호한것이냐... (안해봄)

 

년도별 똥겜

meta_ddong_loc = meta_df.groupby('Year')['Metascore'].idxmin()
meta_df.loc[meta_ddong_loc][['Title', 'Metascore', 'User_Score']].reset_index(drop=True)

? 저거 주술회전 전화항명인가?

 

meta_ddong_loc = meta_df.groupby('Year')['User_Score'].idxmin()
meta_df.loc[meta_ddong_loc][['Title', 'Metascore', 'User_Score']].reset_index(drop=True)

문명 7은 대체 무슨 일이 있었던것이며...

 

장르별 점수(내림차순)

meta_genre_loc = meta_df.groupby('Genre')['Metascore'].idxmax()
meta_df.loc[meta_genre_loc][['Genre', 'Title', 'Metascore', 'User_Score']].reset_index(drop=True).sort_values('Metascore', ascending=False)

장르별로 놓고 봤을때 왕눈하고 엘든링, 발더스게이트, 파타 모르가…저거 뭐임? 쟈들이 공통 1위다. 그 중에서도 발더스게이트는 유저 스코어도 9점을 넘겼다. 당신들 대체 뭘 만든겁니까.

 

best_idx = meta_df.dropna(subset=['User_Score']).groupby('Genre')['User_Score'].idxmax()
best_genre_df = meta_df.loc[best_idx]

best_genre_df[['Genre', 'Title', 'Metascore', 'User_Score']].reset_index(drop=True).sort_values('User_Score', ascending=False)

33원정대도 갓겜이라고 하더군요... (안해봄)

 

장르별 점수(오름차순)

meta_genre_loc = meta_df.groupby('Genre')['Metascore'].idxmin()
meta_df.loc[meta_genre_loc][['Genre', 'Title', 'Metascore', 'User_Score']].reset_index(drop=True).sort_values('Metascore')

 

best_idx = meta_df.dropna(subset=['User_Score']).groupby('Genre')['User_Score'].idxmin()
best_genre_df = meta_df.loc[best_idx]

best_genre_df[['Genre', 'Title', 'Metascore', 'User_Score']].reset_index(drop=True).sort_values('User_Score')

0.5점은 대체...

 

리뷰 많은 순

most_review = meta_df.sort_values('Critic_Review_Count', ascending=False)
most_review[['Title', 'Critic_Review_Count', 'User_Review_Count', 'Metascore', 'User_Score']]

일단 비평가 리뷰는 왕눈이 제일 많다. 여기서 유저 리뷰도 5000개 이상인 게임들만 묶어서 보면

# 유저 리뷰가 5000개 이상인 게임들만 골라봤음
most_review = meta_df.sort_values('Critic_Review_Count', ascending=False)
most_review.query('User_Review_Count >= 5000')[['Title', 'Critic_Review_Count', 'User_Review_Count', 'Metascore', 'User_Score']]

그래도 왕눈이 제일 많다. 제작진이 과학상자를 던져준 탓에 구하라는 하이랄은 안 구하고 과학상자 갖고 노는 게임… ㅋㅋㅋㅋ 누가 그걸로 고질라 만들었더만.

 

most_review = meta_df.sort_values('User_Review_Count', ascending=False)
most_review[['Title', 'Critic_Review_Count', 'User_Review_Count', 'Metascore', 'User_Score']]

넥슨이 공들여서 만들었는데 카잔도 리뷰좀 해줘라... 유저 리뷰는 33원정대, 엘든링, 발더스게이트 3 셋 다 2만개를 넘어갔다. 이 중에서도 비평가 리뷰가 100개 이상인 게임들만 보면

# 비평가 리뷰가 100개 이상인 게임
most_review = meta_df.sort_values('User_Review_Count', ascending=False)
most_review.query('Critic_Review_Count >= 100')[['Title', 'Critic_Review_Count', 'User_Review_Count', 'Metascore', 'User_Score']]

메트로이드 프라임은 왜 리뷰가 없는겨…

반응형

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

얘! clinvar도 EDA가 된단다! (1)  (0) 2026.02.15
Ramen ratings  (0) 2026.02.11
또 ChEMBL을 털어보았다  (0) 2026.01.28
캐글 EDA-마! 서퍼티파이! (2)  (0) 2026.01.20
캐글 EDA-마! 서퍼티파이!  (0) 2026.01.19
Lv. 36 라이츄

Lv. 36 라이츄

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

코로나바이러스 MSA

Coding/Python 2026. 2. 5. 19:10
반응형

나도 이걸 4번까지 하게 될 줄은 몰랐음…


개요

여러분들 다들 아시죠? 코로나19가 우리를 어떻게 변화시켰는지...

 

저는 자가격리도 해보고 걸려도 봤습니다. 자가격리때 다들 헐 어카냐 했는데… 저는 태생이 집순이라 1도 데미지 없었고요.. 격리소가 을지로 근처라 원격으로 명동성당 루기아 줘팼습니다.

 

창고는 언제나 열려있소

# 쿼리 조건: SARS-CoV-2 (코로나19), Spike 단백질 위주로 털어보기
# 2025년 최신 데이터 + 사람 숙주 조건
query = "SARS-CoV-2 AND S[Gene Name] AND 2025[PDAT] AND Homo sapiens[Host]"

# 1. ID 리스트 가져오기
handle = Entrez.esearch(db="nucleotide", term=query, retmax=300)
record = Entrez.read(handle)
id_list = record["IdList"]
handle.close()

# 2. 실제 서열 데이터 가져오기 (FASTA 형식)
fetch_handle = Entrez.efetch(db="nucleotide", id=id_list, rettype="fasta", retmode="text")
sequences = list(SeqIO.parse(fetch_handle, "fasta"))
fetch_handle.close()

# 3. 저장 
with open("influenza_h3n2.fasta", "w") as f:
    SeqIO.write(sequences, f, "fasta")

print(f"성공적으로 {len(sequences)}개의 서열을 가져왔습니다.")
print("----------")

for record in sequences[:3]:
    print(f"ID: {record.id}")
    print(f"Description: {record.description}")
    print(f"Length: {len(record.seq)} bp\n")

...근데 저걸 꼭 2025년만 가져오는 이유가 있는거니 제미나이야...?

 

# 콤퓨타에 저-장
vir_sequence = []
for i, id in enumerate(id_list):
    print(f"Downloading sequence {i+1}/{len(id_list)}: {id}")
    handle = Entrez.efetch(db="nucleotide", id=id, rettype="fasta", retmode="text")
    record = SeqIO.read(handle, "fasta")
    
    # record에 id와 seq가 다 들어가야되더라... (안되면 오류남 봤음)
    vir_sequence.append(record) 

# 파일로 저장
SeqIO.write(vir_sequence, "hantavirus_sequence.fasta", "fasta")
print('Done!')
# 시퀀스 길이 체크 
for rec in vir_sequence:
    print(f"ID: {rec.id} | Length: {len(rec.seq)}")

이게 그… 체크해봤더니 완전 잡탕찌개인거예요… 뭔 닥터 스트레인지 왔다감도 아니고 대혼돈의 유니버스여 게놈이… 저게 전체 데이터는 한 다섯자리 되고, 중간중간 3~4000bp정도 되는게 바이러스 스파이크(그 겉면에 삐죽삐죽한 작대기 있음)인데, 저걸 저대로 MSA하면 일단 똥때리고 밥먹고 자다 인나서 다음날이 됐는데도 MSA가 안 끝났을 확률이 99.9%입니다. 그리고 우리 스파이크만 할거임.

 

# 가져온 sequences 리스트에서 길이로 필터링
spike_only = [rec for rec in sequences if 3000 <= len(rec.seq) <= 4500]

# 필터링된 결과 저장
with open("sars_cov2_spike_clean.fasta", "w") as f:
    SeqIO.write(spike_only, f, "fasta")

print(f"전체 {len(sequences)}개 중 진짜 스파이크만 {len(spike_only)}개 골라냈습니다.")

그래서 어떻게 해요? 아 스파이크만 거르셔야죠.

 

MSA

print('MSA start... ')

# MSA 분석 시-작
try: 
    result = subprocess.run([muscle_exe, "-align", "sars_cov2_spike_clean.fasta", "-output", "sars_cov2_spike_align.fasta"], check=True, capture_output=True, text=True)
    print("Completed. ")
except subprocess.CalledProcessError as e: 
    print(f"MSA failed: {e}")
finally:
    alignment = AlignIO.read("influenza_h3n2_muscle_aligned.fasta", "fasta")

# 밥 먹고 오면 끝나있겠는데...?

보통 돌려놓고 똥때리고 오면 끝나있습니다. 담배는 안되냐고요? 뭐 알아서 피시든지... 나한테 냄새만 안 오게 하십쇼.

 

print("====== MSA Result ======")
alignment = AlignIO.read("sars_cov2_spike_align.fasta", "fasta") # FASTA 니네 확장자가 몇개냐... 

for record in alignment:
    print(f"{record.id[:10]:<15} : {record.seq[:100]}")
====== MSA Result ======
PV687028.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687026.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687025.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687024.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687027.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAAATCAATC---------ATACACTA
PV687011.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687009.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687016.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687015.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687014.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687012.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687023.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687022.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687019.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687010.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687013.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687021.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687020.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687018.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PV687017.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGTCATGCCGCTGTTTAATCTTATAACTACAACTCAATC---------ATACACTA
PX775888.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGT------------TAATCTTAGAACCAGAACTCAATTACCCCCTGCATACACTA
PX765932.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGT------------TAATCTTACAACCAGAACTCAATTACCCCCTGTATATATTA
PV687008.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGT------------TAATCTTATAACCAGAACTCAATC---------ATACACTA
PV687007.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGT------------TAATCTTATAACCAGAACTCAATC---------ATACACTA
PX765940.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGT------------TAATCTTACAACCAGAACTCAATTACCCCCTGCATACACTA
PX736381.1      : ATGTTTGTTTTTCTTGTTTTATTGCCACTAGTCTCTAGTCAGTGTGT------------TAATCTTACAACCAGAACTCAATTACCCCCTGTATATATTA

다시 말씀드리는거지만 저 작대기는 갭이고요... 여 염기 읍따! 입니다... 예...

 

평균 보존율

def calculate_conservation_no_gap(alignment, gap_threshold=0.5):
    length = alignment.get_alignment_length()
    scores = []

    for i in range(length):
        column_raw = alignment[:, i]

        # gap 비율이 너무 높으면 제외 (선택사항)
        gap_fraction = column_raw.count("-") / len(column_raw)
        if gap_fraction > gap_threshold:
            continue

        # gap 제거
        column = column_raw.replace("-", "")
        if len(column) == 0:
            continue

        # 최빈 염기 비율 = 보존도
        most_common = max(set(column), key=column.count)
        score = column.count(most_common) / len(column)
        scores.append(score)

    return scores

scores = calculate_conservation_no_gap(alignment)

print(f"해당 구간의 평균 보존율: {np.mean(scores)*100:.2f}%")
해당 구간의 평균 보존율: 99.53%

너 바이러스잖아...

 

섀넌 엔트로피

plt.figure(figsize=(15, 7))
plt.plot(variation_scores, alpha=0.8, color='#00498c')
plt.fill_between(range(len(variation_scores)), variation_scores, alpha=0.3)

plt.axvline(1322, linestyle='--', color='red', alpha=0.5)
plt.text(1322, max(variation_scores)*1.07, 'Spike POS 1322', color='red', alpha=0.5, ha='center', fontweight='bold')

plt.axvline(3295, linestyle='--', color='red', alpha=0.5)
plt.text(3295, max(variation_scores)*1.07, 'Spike POS 3295', color='red', alpha=0.5, ha='center', fontweight='bold')

plt.axvline(626, linestyle='--', color='red', alpha=0.5)
plt.text(626, max(variation_scores)*1.07, 'Spike POS 626', color='red', alpha=0.5, ha='center', fontweight='bold')

plt.title("Viral Variation Hotspots", y = 1.07)
plt.xlabel("Alignment Position (filtered)")
plt.ylabel("Normalized Shannon Entropy")
plt.show()

저 그래프 코드에 axvline 세 개 달려있는게 핫스팟 TOP 3임. 저걸 한타 인플루엔자 라이노바이러스까지 다 넣느라 좀 걸렸습니다... 솔직히 있으면 좋잖아.

 

# 섀넌 엔트로피 점수 도출
def get_top_variable_sites_no_gap(alignment, top_n=10):
    length = alignment.get_alignment_length()
    variability = []

    ref_seq = alignment[0].seq

    for i in range(length):
        # 🔴 reference가 gap이면 무조건 스킵
        if ref_seq[i] == '-':
            continue

        column = alignment[:, i].replace("-", "")
        if not column:
            continue

        counts = Counter(column)
        total = sum(counts.values())

        entropy = 0.0
        for c in counts.values():
            p = c / total
            entropy -= p * math.log2(p)

        variability.append((i, entropy))

    return sorted(variability, key=lambda x: x[1], reverse=True)[:top_n]

def alignment_to_sequence_pos(aligned_seq, aln_pos):
    count = 0
    for i in range(aln_pos + 1):
        if aligned_seq[i] != '-':
            count += 1
    return count


ref_seq = alignment[0].seq
top_sites = get_top_variable_sites_no_gap(alignment, top_n=10)

high_entropy_ha_sites = []

print("--- 변이가 집중된 주요 포지션 분석 결과 ---")
for aln_pos, score in top_sites:
    real_pos = alignment_to_sequence_pos(ref_seq, aln_pos)
    high_entropy_ha_sites.append(real_pos)
    print(f"Alignment {aln_pos:4d} → Spike Pos {real_pos:4d} | 엔트로피: {score:.3f}")

print("\n최종 고엔트로피 포지션 리스트:")
print(high_entropy_ha_sites)
--- 변이가 집중된 주요 포지션 분석 결과 ---
Alignment 1345 → Spike Pos 1322 | 엔트로피: 1.212
Alignment 3321 → Spike Pos 3295 | 엔트로피: 0.999
Alignment  649 → Spike Pos  626 | 엔트로피: 0.991
Alignment 1391 → Spike Pos 1368 | 엔트로피: 0.991
Alignment 1377 → Spike Pos 1354 | 엔트로피: 0.961
Alignment 1488 → Spike Pos 1462 | 엔트로피: 0.931
Alignment 1720 → Spike Pos 1694 | 엔트로피: 0.929
Alignment 2053 → Spike Pos 2027 | 엔트로피: 0.893
Alignment 1048 → Spike Pos 1025 | 엔트로피: 0.840
Alignment 3455 → Spike Pos 3429 | 엔트로피: 0.795

최종 고엔트로피 포지션 리스트:
[1322, 3295, 626, 1368, 1354, 1462, 1694, 2027, 1025, 3429]

저기서 엔트로피가 가장 높은 포인트 세 개를 axvline으로 넣은겁니다. 가릿?

 

맨-휘트니 U 검정

# --- 2. normalization ---
entropy_min = np.nanmin(entropy_window)
entropy_max = np.nanmax(entropy_window)

entropy_window_normalized = (
    entropy_window - entropy_min
) / (entropy_max - entropy_min)

variation_scores = entropy_window_normalized

# --- 3. NaN 제거 (🔥 중요) ---
valid_scores = variation_scores[~np.isnan(variation_scores)]

# --- 4. hotspot threshold (normalized 기준) ---
threshold_norm = np.percentile(valid_scores, 90)
threshold_raw = threshold_norm * (entropy_max - entropy_min) + entropy_min

hotspots = valid_scores[valid_scores >= threshold_norm]
non_hotspots = valid_scores[valid_scores < threshold_norm]

u_stat, p_value = mannwhitneyu(
    hotspots,
    non_hotspots,
    alternative="greater"
)

print(f"Hotspot threshold (top 10%, normalized entropy): {threshold_norm:.3f}")
print(f"Corresponding raw entropy threshold: {threshold_raw:.3f}")
print(f"Mann–Whitney U statistic: {u_stat:.1f}")
print(f"p-value: {p_value:.4e}" if p_value > 1e-10 else "p-value: <1e-10")
Hotspot threshold (top 10%, normalized entropy): 0.235
Corresponding raw entropy threshold: 0.056
Mann–Whitney U statistic: 1309496.0
p-value: <1e-10

거 깃헙 가보시면 통계값도 있으니까 함 보세요... 아무튼 귀무가설(코로나바이러스 스파이크의 변이는 무작위적으로 발생하며, 특정 위치에 선호적으로 집중되지 않는다.) 압도적 기각. 근데 시각화한 거 보셨으면 대충 귀무가설 기각될 것 같긴 했죠? 그래프에 수평선이 보이니까.

 

Phylogenic tree

코드는 네이버에서 짤릴까봐 뺐습니다. 이것도 깃헙가서 보세요. 근데 트리 코드는 딱히 바뀐게 없음…

 

스피어맨 상관계수

그 우리 인플루엔자 할 때도 트리 관련해서 맨 휘트니 안했죠? 그때 내가 걔들은 같은 아종에서 바뀌는거라 맨 휘트니 하면 결과 이상하게 나온다고 했잖아요. 근데 얘네도 비슷해요 이유는. 스파이크 단백질이 어어하면 숙주 찾아서 입장하기도 전에 뽀사지거든… 그래서 보존구역이 많은거고, 그래서 맨 휘트니를 못합니다.

 

rho, p = spearmanr(tree_distances, seq_identities)

print(f'Rho: {rho:.4f}')
print(f'P-value: {p:.4e}')
Rho: -0.9950
P-value: 0.0000e+00

얘도 음의 상관관계네 ㅋㅋㅋㅋ 

 

plt.figure(figsize=(10, 7), dpi=120)
sns.regplot(x=tree_distances, y=seq_identities, 
            scatter_kws={'alpha':0.2, 'color':'gray', 's':10}, 
            line_kws={'color':'red', 'label': f'Spearman Rho: {rho:.3f}'})

plt.title("Coronavirus: Tree Distance vs Sequence Identity", fontsize=15, pad=15)
plt.xlabel("Genetic Distance on Tree", fontsize=12)
plt.ylabel("Pairwise Sequence Identity", fontsize=12)
plt.legend()
plt.grid(True, linestyle='--', alpha=0.5)
plt.show()

오… 이게 봐봐요. 계통수에서 거리가 멀수록 진화적으로 먼? 뭐 그런거잖아요? 그래서 스피어만 상관계수가 음의 상관계수가 나온거다. 트리의 거리가 멀어질수록 유사성도 떨어지잖아요.

반응형
Lv. 36 라이츄

Lv. 36 라이츄

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

씨본 컬러 파레트 씨뮬레이터

Coding/JavaScript 2026. 1. 30. 23:05
반응형

그 Seaborn에서 컬러 커마 가능한거 아시죠? 근데 이게 뭔 색인지 뭔 파레트가 어떻게 나오는지 모르잖아요. ㅇㅋ? ㅇㅇㅋ. 그래서 색 두 개를 입력받은 다음 한 10칸정도로 띄엄띄엄 칠한거 하나, cmap(이어지는거)으로 하나 짜잔 하고 보여주자 이거다. 


이거 근데 팀플에서도 써먹으려면 코드펜에도 올려야 할 듯.


<html>
    <head>
        <title>Seaborn palette simulator</title>
        <link href="style.css" rel="stylesheet">
    </head>
    <body>
        <div class="wrapper">
            <h1>Seaborn palette simulator</h1>
            <p>그... 저기 인풋창에 색깔 두개 입력하시면 파레트랑 cmap이랑 보여줄거긴 합니다. 근데... 아... </p>
            <p>세개 이상... 그거는 제 능력 밖이니까 걍 두개씩 돌려보세요... <s>이봐요</s></p>
            <div class="color_input"></div>
            <div class="palette">

            </div>
            <div class="cmap"></div>
        </div>
    </body>
    <script src="script.js"></script>
    <script src="https://kit.fontawesome.com/dc58858c96.js" crossorigin="anonymous"></script>
</html>

그래요. 여기에도 썼지만 세개 이상은 내 능력 밖이니께 걍 두개씩 보든가 하세요.

 

cmap_gradation = document.querySelector('.cmap');
palette_gradation = document.querySelector('.palette');
color_button = document.querySelector('#generate'); // Button

일단 파레트는 아래에 div를 10개정도 생성하고 색을 동적으로 줘야 하고, cmap은 걍 그라데이숑 주면 된다. 그라데이션이 쉽죠. 배경만 입히면 되니까. 그럼 걍 그라데이션 하면 안되나 할 수도 있는데 씨본 파레트가 딱딱 끊어지는 구조입니다.

 

cmap_gradation = document.querySelector('.cmap');
palette_gradation = document.querySelector('.palette');
// Input
first_color = document.querySelector('#first_color');
second_color = document.querySelector('#second_color');

color_button = document.querySelector('#generate'); // Button

// 벗흔이여 일을 하세요
color_button.addEventListener('click',()=>{
    console.log(first_color.value, second_color.value)
});

뭔가 가져온 게 늘은 것 같죠? 늘은거 맞음. 인풋창을 갖고와야 색을 제어하죠.

 

// 벗흔이여 일을 하세요
color_button.addEventListener('click',()=>{
    // 칸으로 노나지는 그거 
    palette_div = document.createElement('div');
    palette_div.classList.add('palette_div');
    // 그라데이션
    cmap_div.style.backgroundImage = `linear-gradient(to right, ${"#" + first_color.value}, ${"#" + second_color.value})`; // 왼->오
});

그라데이션이 간단할거라고 생각하던 시절이 나에게도 있었다. 저거 적용하고 텍스트 달고 하는게 개노가다였지만 그라데이션은 선녀예요. 파레트는 저 전체 크기를 10등분한 걸 생성해서 줄줄이 붙여야됩니다... ㅋㅋㅋㅋㅋㅋ

 

const hexToRgb = (hex) => {
    const r = parseInt(hex.substring(0, 2), 16);
    const g = parseInt(hex.substring(2, 4), 16);
    const b = parseInt(hex.substring(4, 6), 16);
    return [r, g, b];
};
const startRgb = hexToRgb(first_color.value);
const endRgb = hexToRgb(second_color.value);
const n = 10;

//자 드가자 
for (let i = 0; i < n; i++) {
    const step = i / (n - 1); // 0부터 1까지의 비율

    // R, G, B 각각 보간 계산
    const r = Math.round(startRgb[0] + (endRgb[0] - startRgb[0]) * step);
    const g = Math.round(startRgb[1] + (endRgb[1] - startRgb[1]) * step);
    const b = Math.round(startRgb[2] + (endRgb[2] - startRgb[2]) * step);

    // 새 div 생성 및 스타일 적용
    const chip = document.createElement('div');
    chip.classList.add('palette_chip'); // CSS에서 너비/높이 설정용
    chip.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
    chip.style.flex = "1"; // 10칸이 골고루 나눠지도록

    // 드디어 등장하는 오타 없는 그 녀석
    palette_div.appendChild(chip);

생성되는거 방향 뻑나서 플렉스 줬다... 그럼 여기서 끝인가요? 아뇨. 유효성검사 해야죠.

 

// 벗흔이여 일을 하세요
color_button.addEventListener('click',()=>{
    if (!regex.test(first_color.value) || !regex.test(second_color.value)) {
        alert('유효한 색상값이 아닙니다!')
    } else {
        palette_div.innerHTML = '';
        cmap_div.innerHTML = '';
        // 칸으로 노나지는 그거 
        const hexToRgb = (hex) => {
            const r = parseInt(hex.substring(0, 2), 16);
            const g = parseInt(hex.substring(2, 4), 16);
            const b = parseInt(hex.substring(4, 6), 16);
            return [r, g, b];
        };
        const startRgb = hexToRgb(first_color.value);
        const endRgb = hexToRgb(second_color.value);
        const n = 10;

        //자 드가자 
        for (let i = 0; i < n; i++) {
            const step = i / (n - 1); // 0부터 1까지의 비율

            // R, G, B 각각 보간 계산
            const r = Math.round(startRgb[0] + (endRgb[0] - startRgb[0]) * step);
            const g = Math.round(startRgb[1] + (endRgb[1] - startRgb[1]) * step);
            const b = Math.round(startRgb[2] + (endRgb[2] - startRgb[2]) * step);

            // 새 div 생성 및 스타일 적용
            const chip = document.createElement('div');
            chip.classList.add('palette_chip'); // CSS에서 너비/높이 설정용
            chip.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
            chip.style.flex = "1"; // 10칸이 골고루 나눠지도록

            // 드디어 등장하는 오타 없는 그 녀석
            palette_div.appendChild(chip);
        }
    }
    // 그라데이션
    cmap_div.style.backgroundImage = `linear-gradient(to right, ${"#" + first_color.value}, ${"#" + second_color.value})`; // 왼->오
    
});

나 그러고보니 처음에 요소 가져올때 const도 안 붙이고 가져왔네?

 

마진 좀 조절하고 깃헙에 올리겠음. 


See the Pen Seaborn palette simulator by koreanraichu (@koreanraichu) on CodePen.

 

코드펜에도 올렸음다.

반응형
Lv. 36 라이츄

Lv. 36 라이츄

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

또 ChEMBL을 털어보았다

Coding/EDA 2026. 1. 28. 17:25
반응형

오늘은 좀 다채롭습니다. 왜냐고? 뭐가 많이 들어서.


그 우리 이제 할만큼 하셨으니까... 전처리는 다 안올릴게여... 내가 귀찮아서 이러는거 맞음. 내가 네이버 티스토리 미디움에 동시에 올리는데 그러면 복붙을 몇 번 하는겁니까...


배경설명

이번에 가져온 데이터베이스의 주제가 Kinase Inhibitor입니다. 이게 뭔데요? 를 설명하려면 kinase와 inhibitor를 다 설명해야 하는데… 아…

 

Kinase는 뭐에 인산기를 붙이는 효소입니다. 인산기는 PO4(3-)고, 그걸 어디다가 붙이는 놈들이 다 카이네이스예요. 인산기를 어디다가 붙이는가는 효바효(효소 바이 효소)지만 어쨌든 인산기를 갖다 붙입니다. 보통 ATP에서 떼다가 붙이긴 합니다만.

 

자, 그럼 인히비터에 대한 얘기를 해봅시다. 우리 몸의 모든 것은 신호체계를 기반으로 돌아갑니다. 뭘 대사하는 과정, 숨쉬는것, 세포분열까지 하나한 신호체계가 관여하고 있고, 그 신호체계에 끼어드는 놈이 kinase다 이겁니다. 당연하게도 이 신호체계는 멈춰야 할 때 멈추고, 돌아야 할 때 도는 게 정상입니다. 적절하게 엑셀/브레이크가 제 역할을 해 줘야 한다 이거죠.

 

그런데 이 엑셀과 브레이크가 선천적으로 없거나(유전병이라던가...), 후천적으로 없어진다면(돌연변이 누적)? 네. 질병입니다. 암도 여러 돌연변이가 누적되어서 엑셀, 브레이크가 다 맛이 가면 생기게 되는겁니다. 그럼 인히비터는 여기서 뭘 하는데요? 엇나간 엑셀, 엇나간 브레이크가 신호체계 개발살내는 걸 방해하는겁니다.

 

kinase가 다양한 곳에서 다양한 역할을 하기 때문에, 그 kinase를 타겟으로 하는 약물들도 다양합니다.


전처리

임상단계 범주화

df['Status'] = df['Max Phase'].map({4: 'Approved', 3: 'Clinical', 2: 'Clinical', 1: 'Clinical', 0: 'Clinical', -1: 'Failed'})

전에 이부프로펜 보고 오신 분들은 뭔지 바로 아실듯... 예. 그겁니다. Max Phase가 4면 승인됨, 3~0까지는 임상(0은 엄밀히 말하자면 전임상), -1은 엎음. 참고로 저기서 0 한번도 못봤음...

 

Status
Clinical    1246
Approved     451
Failed       116
Name: count, dtype: int64

이부프로펜보다 쪽수가 훨배 많음둥..

 

분자 타입 카테고리화

얘가 이부프로펜보다 쪽수도 많고 카테고리도 많거든요? 함 보실?

Type
Small molecule             1727
Unknown                      50
Protein                      21
Oligosaccharide               6
Oligonucleotide               4
Antibody drug conjugate       3
Antibody                      2
Vaccine component             1
Gene                          1
Enzyme                        1
Name: count, dtype: int64

그래서 카테고리를 두 단계로 나눌건데

이렇게 나눌겁니다. ㅇㅋ? ㅇㅇㅋ

 

df['Category_1'] = df['Type'].apply(lambda x: "Small molecule" if x == "Small molecule" else "Other")

솔직히 둘로 나누는건 기명함수보다 람다식이 간편하쥬. 

 

# 그룹 정의
groups = {
    'Biologics': ['Protein', 'Antibody', 'Antibody drug conjugate', 'Enzyme', 'Vaccine component'],
    'Nucleic acids': ['Oligonucleotide', 'Gene'],
    'Sugars': ['Oligosaccharide'],
    'Small molecule': ['Small molecule'],
    'Unknown': ['Unknown']
}

# 근데 뒤집을거면 처음부터 반대로 만들면 안되는거임? 
# 안된답니다. 
mapping = {val: key for key, values in groups.items() for val in values}

# 예 적용됐습니다. 
df['Category_2'] = df['Type'].map(mapping)

딕셔너리 만들고 뒤집어서 적용할거면 그냥 리스트를 키에 올리면 안되나요? 안된단다. 딕셔너리 키에 튜플이라면 몰라도 리스트는 못간다.


임상 단계별 분자량 평균

df.groupby('Status')['Molecular Weight'].mean() # 평균
Status
Approved    503.598337
Clinical    487.756129
Failed      598.249815
Name: Molecular Weight, dtype: float64

평균은 이런데... 과연 분포는 어떨까?

 

# boxplot
sns.boxplot(df, x = 'Status', y = 'Molecular Weight', hue = 'Status')
plt.yscale('log')
plt.title('Molecular Weight by Status')
plt.show()

분자량 6000 넘는거 확인해봤더니(수상하게 멀리 튀어있음) 올리고뉴클레오타이드더라... 하나는 클리니컬, 하나는 페일드인데 일단 올리고뉴클레오타이드는 원래 뚠뚠합니다. 인별 릴스 보면 뭐 딸기에서 DNA 뽑기 이런거 하죠? 그게 일단 전체 조직에서 뽑는거기도 하고 DNA가 세포 안에 실패로 감고 감고 감아서 염색체로 짱박아야 할 정도로 뚠뚠함.

 

쟤는 VEGF라는 애가 타겟인데, 타겟인 애가 kinase가 아니고 kinase를 작동하기 위한 열쇠다. 그러니까 자동차로 비유하자면 자동차에 차 키를 못 꽂게 방해하는겁니다.

 

근데 왜 실패했냐고? DNA도 뱃속으로 들어가면 걍 영양원입니다 여러분. 그리고 쟤가 siRNA인데... 그 우리 RNA 뽑을 때 DEPC-증류수 쓰는게 RNase때문입니다. 실험에 써야되는데 걔가 다짤라... 걔를 무력화할 수단 중 하나가 DEPC라서 물에 그걸 타서 쓰는건데... 약 하나 투여하자고 그걸 같이 넣을 순 없잖습니까... GHS에 느낌표 떴다고... 이렇게 말하면 아니! RNA 백신은 그럼 허구냐! 하실 수 있는데, 제약사들도 똥멍청이가 아닙니다. 석박들이 모여서 머리를 맞대고 이거 분해 안되고 배달되게 할 방법 없을까? 해서 방법을 강구했죠. 리포좀이나 인지질같은 걸로 싸서 효소가 접근을 못 하게 하거나, 아예 퀵으로 목적지까지 쏴버리거나, backbone(그 사다리에 양 옆에 1자로 서있는거)을 바꾸거나.

 

바이오액티비티

df.groupby('Status')['Bioactivities'].mean() # 평균
Status
Approved    587.428977
Clinical    185.371498
Failed       33.195122
Name: Bioactivities, dtype: float64

롸?

 

# boxplot
sns.boxplot(df, x = 'Status', y = 'Bioactivities', hue = 'Status')
plt.yscale('log')
plt.title('Bioactivities by Status')
plt.show()

그… 실패한 애들은 어쩌다…

 

AlogP

df.groupby('Status')['AlogP'].mean() # 평균
Status
Approved    3.149507
Clinical    3.531329
Failed      3.429429
Name: AlogP, dtype: float64

평균은 무난하다. 근데 분포도 그럴까?

 

# boxplot
sns.boxplot(df, x = 'Status', y = 'AlogP', hue = 'Status')
plt.title('AlogP by Status')
plt.show()

이렇게 봐서는 그놈이 그놈같으니까 ANOVA 함 해보자.

 

ANOVA

# 표본 수 확인용
# 결측값 안지우면 통계량 안나와요... 
df_approved = df.query('Status == "Approved"')['Bioactivities'].dropna()
df_clinical = df.query('Status == "Clinical"')['Bioactivities'].dropna()
df_failed = df.query('Status == "Failed"')['Bioactivities'].dropna()
f_stats, p_val = stats.f_oneway(df_approved, df_clinical, df_failed)
print(f'F-Statistics: {f_stats:.2f}')

if p_val < 0.05:
    print(f"p-value: {p_val:.2e}: 귀무가설 기각! Tukey 드가자!") # 아 Tucky가 아니구나... 튜키씨 미안... 
else:
    print(f"p-value: {p_val:.2e}: 귀무가설 기각 실패")

근데 왜 피밸류는 f 아니고 e예요? 피밸류가 급나 완전 작게 나올 때(-111e 이런거) f때리면 0으로 나옵니다... 아무 튼 그래서 어떻게 됐냐고?

 

F-Statistics: 52.70
p-value: 1.13e-22: 귀무가설 기각! Tukey 드가자!

뭘 어떻게돼요 튜키 해야돼 우리...

 

all_vals = list(df_approved) + list(df_clinical) + list(df_failed)
all_labels = (['Approved'] * len(df_approved) + ['Clinical'] * len(df_clinical) + ['Failed'] * len(df_failed))

tukey = pairwise_tukeyhsd(endog=all_vals, groups=all_labels, alpha=0.05)
print(tukey)
     Multiple Comparison of Means - Tukey HSD, FWER=0.05     
=============================================================
 group1   group2   meandiff p-adj    lower     upper   reject
-------------------------------------------------------------
Approved Clinical -402.0575    0.0 -497.2978 -306.8172   True
Approved   Failed -554.2339    0.0 -800.9217  -307.546   True
Clinical   Failed -152.1764 0.2947 -391.4005   87.0477  False
-------------------------------------------------------------

누누이 말하는거지만 어노바(분산분석)는 얘네들 중 뭔가 다른데? 만 알려주는거고 튜키까지 해야 뭐가 구체적으로 다른지를 알려줍니다. 여기서는 승인됨 그룹이 다른 두 그룹과 다르다는 걸 튜키가 알려준거임.

 

AlogP의 경우 임상 단계와 상관 없이 6을 넘는 애들이랑 0보다 작은 애들이 있었는데, 일단 6보다 큰 애들은 그만큼 기름지시다는 의미고 0보다 작은 애들은 이건 뭐 물이야 뭐야라고 보시면 된다. 리핀스키 룰에서는 0~5까지로 지정하는데 왜 그러냐면 세포막은 인지질이지만 우리 몸은 물이거든...

 

AlogP가 규격 외인 약물들이  승인 단계에서 가장 많이 보이는 게 의외였음. 일단 임상중에서도 혼자 3상인 SONROTOCLAX 혼자 3상이다. 엥? 뭐지? 너무 기름지면 안된다면서요? 근데 쟤는 타겟이 기름바다에 위치해 있어서 기름져? 오히려 좋아! 인겁니다. AlogP가 낮은 애들 중에는 염기 짝퉁(이미테이션), 항생제(주사로 놓기때문에 기름질 필요가 없음), 올리고당이 보인다. 그러니까 누누이 말하는거지만 RO5를 지키면 좋다는거지 꼭 어길 필요가 없어요… 그 특성과 타겟에 따라서는 오히려 좋아! 인 경우도 있거든. 승인된 약물 중 EPTIFIBATIDE는 뱀독 유래 펩타이드다.

 

부작용 플래그

df.query('`Withdrawn Flag` != False')[['Name','Status','Type', 'AlogP','Type','Withdrawn Flag']]
	Name	Status	Type	AlogP	Type	Withdrawn Flag
445	LUMIRACOXIB	Approved	Small molecule	4.16	Small molecule	True
473	TOLRESTAT	Approved	Small molecule	3.56	Small molecule	True
504	CERIVASTATIN	Approved	Small molecule	4.88	Small molecule	True
564	XIMELAGATRAN	Approved	Small molecule	1.10	Small molecule	True
849	VALDECOXIB	Approved	Small molecule	2.96	Small molecule	True
954	UMBRALISIB	Approved	Small molecule	6.66	Small molecule	True
1160	CERIVASTATIN SODIUM	Approved	Small molecule	4.88	Small molecule	True
1389	NOMIFENSINE MALEATE	Approved	Small molecule	2.85	Small molecule	True
1427	NOMIFENSINE	Approved	Small molecule	2.85	Small molecule	True
1548	TOLCAPONE	Approved	Small molecule	2.55	Small molecule	True
1578	APROTININ	Approved	Unknown	NaN	Unknown	True

그… 전에도 얘기했지만 모든 약에는 부작용이 있어요. 근데 여기 플래그가 선 건 뭐다? 부작용이 진지하게 이거 투약하면 X될 것 같다… 수준이라는 얘깁니다.

 

1. CERIVASTATIN: 횡문근융해증(그 운동 무리하면 콜라색 소변 보잖아요? 그겁니다)
2. LUMIRACOXIB: 간독성
3. VALDECOXIB: 심혈관 질환(심장마비, 뇌졸중)의 위험성 증가 및 심각한 피부 반응 위험
4. XIMELAGATRAN: 와파린(항응고제)의 대체제였는데 아... 간독성 아...
5. TOLRESTAT: 간독성
6. UMBRALISIB: 어... 그... 항암제인데요... 다른 의미로 효과가 있었습니다... 
7. NOMIFENSINE MALEATE: 항우울제인데... 아...
    - 급성 용혈성 빈혈: 내 몸의 면역계가 갑자기 내 적혈구를 적으로 오해해서 다 터뜨려버립니다. (피가 모자라!)
    - 노미펜신 열(Fever): 약만 먹으면 고열이 펄펄 끓습니다.
    - 심각한 간 독성: 간 수치가 수직 상승합니다.
    - 조증/환각: 정신과 약(항우울제)인데, 우울증 고치려다 사람이 너무 들떠서 조증이 오거나 환각을 봅니다.
8. TOLCAPONE: 간독성
9. APROTININ: 피를 너무 잘 멈춰서... 아...

 

RO5 violation

df.query('Category_1 == "Small molecule"').groupby(['Status', '#RO5 Violations']).size().unstack()

그 원래 뚠뚠한 애들이 있지 않나요? 그래서 아예 분자만 필터링해서 본겁니다.

 

#RO5 Violations	0.0	1.0	2.0	3.0
Status				
Approved	278	87	56	3
Clinical	695	291	148	20
Failed	71	22	7	4

삼진아웃(3.0)은 대부분 분자량, HBA, HBD, AlogP 중 세 개가 위반인 케이스다. 저기 있는 숫자가 뭔 의미냐면 룰 중에서 몇 개를 위반했느냐임.

 

언노운, 그리고 Gene

언노운

말 그대로 이걸 뭘로 분류할 지 대단히 애매한 놈들이다. 보면 면역글로불린도 있고 뭐 어쩌고팁 어쩌고닙 많아요... 그리고 보면 아 언노운일 만 했다 싶음...

 

Gene

	Name	Synonyms	Status
716	IGRELIMOGENE LITADENOREPVEC	IGRELIMOGEN LITADENOREPVEC|IGRELIMOGENE LITADE...	Clinical

뭔 유전자 치료 해요? 웬 유전자? 나도 그렇게 생각했는데, 쟤는 바이러스다.

 

아니 그런걸 써요? 라고 생각하실 수도 있는데, 쟤는 유전자 개조한 녀석이라 병을 일으키는 대신 암세포를 찾아가서 핑을 찍는다.

반응형

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

Ramen ratings  (0) 2026.02.11
Post-COVID Video Games Worldwide (2021-2025)  (0) 2026.02.10
캐글 EDA-마! 서퍼티파이! (2)  (0) 2026.01.20
캐글 EDA-마! 서퍼티파이!  (0) 2026.01.19
그냥 해보는 ChEMBL EDA  (0) 2026.01.15
Lv. 36 라이츄

Lv. 36 라이츄

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

라이노바이러스 유전자로 MSA를 해보았다

Coding/Python 2026. 1. 27. 20:30
반응형

얘는 스케일이 좀 다운됐음... 왜냐고요? 게놈이 7500bp거든요. 이걸 인플루엔자나 한타때처럼 2~300개 돌린다? 켜놓고 자고 일어나야됩니다. 아니 리눅스로 하셨어요? 걔로 하면 중간에 뻗음. 맥북으로 돌린건데도 이정돕니다.


쟤는 또 뭐 하는 애임?

여러분 감기랑 독감이랑 다릅니다. 단순히 증상이 다른게 아니라 원인 병원체가 달라요. 독감은 인플루엔자가 원인이고 감기는 라이노바이러스라는 놈이 원인이거든요? 다른 바이러스도 있다만.

 

그거 아십니까? 감기에는 약이 없음. 아니 저희 병원가면 약 주는데요? 그건 '증상을 완화시키는' 약이지 감기 바이러스를 조지는 약이 아닙니다. 아니 그럼 감기약이라고 하면 안되는거 아닌가요? 진정하십쇼. 감기 바이러스는 스포닝풀에서 저글링 뽑아내는것처럼 캐많아요. 그걸 일일이 공격하기가 너무 힘들어.

 

그래서 감기약은 딜러가 아닌 서폿입니다. 증상을 좀 완화시켜서 면역계가 바이러스를 조질 수 있게 돕는 역할임.


창고털이

# 바이러스 서열 다운로드
# 이게 근데 막 받으면 안되거든요? 한타바이러스 식구들은 둘쨰치고 저기가 데이터가 진짜 방대해요. 
virus_query = "Rhinovirus[Organism] AND VP1 AND complete cds AND 7000:7500[Sequence Length]"

print('Searching sequences... ') # 솔직히 이거 없으면 되는건지 불안하잖아요...

handle = Entrez.esearch(db="nucleotide", term=virus_query, retmax=40)
record = Entrez.read(handle)
id_list = record['IdList']

print(f"총 {len(id_list)}개의 표준 서열을 찾았습니다.")

위에도 얘기했지만 얘는 40개 돌리는데도 10분 걸려서 200개 300개 할거면 걍 켜놓고 자야됩니다. 양해 바람.

 

# 콤퓨타에 저-장
vir_sequence = []
for i, id in enumerate(id_list):
    print(f"Downloading sequence {i+1}/{len(id_list)}: {id}")
    handle = Entrez.efetch(db="nucleotide", id=id, rettype="fasta", retmode="text")
    record = SeqIO.read(handle, "fasta")
    
    # record에 id와 seq가 다 들어가야되더라... (안되면 오류남 봤음)
    vir_sequence.append(record) 

# 파일로 저장
SeqIO.write(vir_sequence, "rhinovirus_sequence_20.fasta", "fasta")
print('Done!')
# 시퀀스 길이 체크 
for rec in vir_sequence:
    print(f"ID: {rec.id} | Length: {len(rec.seq)}")

한타꺼 복붙한거라 크게 볼 건 없음.

 

MSA

print('MSA start... ')

# MSA 분석 시-작
try: 
    result = subprocess.run([muscle_exe, "-align", "rhinovirus_sequence_20.fasta", "-output", "rhinovirus_muscle_aligned.fasta"], check=True, capture_output=True, text=True)
    print("Completed. ")
except subprocess.CalledProcessError as e: 
    print(f"MSA failed: {e}")
finally:
    alignment = AlignIO.read("influenza_h3n2_muscle_aligned.fasta", "fasta")

# 밥 먹고 오면 끝나있겠는데...?

이것도 한타거랑 소스는 같은데 게놈 길이때문에 개같이 오래 걸리는겁니다. 돌려놓고 똥싸고 오시면 끝나있음.

 

print("====== MSA Result ======")
alignment = AlignIO.read("influenza_h3n2_muscle_aligned.fasta", "fasta") # FASTA 니네 확장자가 몇개냐... 

for record in alignment:
    print(f"{record.id[:10]:<15} : {record.seq[:100]}")

이거... 올려드리고 싶었는데요... 일단 출력이 짤렸고요... 이거 올려도 네이버에서는 짤림.

 

# 코어 시퀀스는 어디? (바이러스라고 앞뒤 안가리고 다 변형하는거 아님)
def calculate_conservation(alignment):
    length = alignment.get_alignment_length()
    scores = []
    for i in range(length):
        column = alignment[:, i]
        most_common = max(column, key=column.count)
        score = column.count(most_common) / len(column)
        scores.append(score)
    return scores

scores = calculate_conservation(alignment)

print(f"해당 구간의 평균 보존율: {np.mean(scores)*100:.2f}%")

아니 근데 보존율이 왜 98퍼가 뜸? 내가 로직을 조졌나?

 

섀넌 엔트로피

def calculate_shannon_entropy(alignment):
    entropy_list = []
    num_sequences = len(alignment)
    alignment_length = alignment.get_alignment_length()

    for i in range(alignment_length):
        column = alignment[:, i]
        # 각 염기(A, C, G, T, -)의 빈도 계산
        counts = {base: column.count(base) for base in "ACGT-"}
        entropy = 0
        for base in counts:
            p = counts[base] / num_sequences
            if p > 0:
                entropy -= p * math.log2(p)
        entropy_list.append(entropy)
    return entropy_list

# 1. MSA 결과 불러오기 (파일명을 맞춰주세요)
alignment = AlignIO.read("rhinovirus_muscle_aligned.fasta", "fasta")
entropy_values = calculate_shannon_entropy(alignment)

# 2. Sliding Window로 평활화 (가독성 향상)
window_size = 50
smoothed_entropy = np.convolve(entropy_values, np.ones(window_size)/window_size, mode='same')

# 3. 시각화 (NanumSquare 반영)
plt.figure(figsize=(15, 5))
plt.plot(smoothed_entropy, color='#2c3e50', linewidth=1)
plt.title(f'라이노바이러스 게놈 섀넌 엔트로피 분석 (Window Size: {window_size})', fontsize=15, pad=20)
plt.xlabel('게놈 위치 (Nucleotide Position)', fontsize=12)
plt.ylabel('엔트로피 (Bits)', fontsize=12)
plt.grid(True, alpha=0.3)
plt.tight_layout()

plt.show()

아, 이건 한타랑 코드 다릅니다. 한타꺼 그대로 해봤는데 한 1700bp에서 짤리데... 그래서 그래프도 달라요.

 

한타때는 뭔 시뻘건 잔디같은거 하나 있었죠? 걔는 잔디 높이가 높을수록 변이가 잘 되는 지역이었고 쟤는 피크가 위로 솟을수록 변이가 잘 되는 구역인겁니다. 보시면 피크가 들쭉날쭉하죠? 그리고 저기 수상학 낮은 부분도 보이죠? 그죠. 쟤들도 아무데나 변이하면 X되는거예요.

 

# 여러분 이것도 통계분석이 됩니다. 
variation_scores = np.array(entropy_values)

mean_var = np.mean(variation_scores)
median_var = np.median(variation_scores)
iqr_var = np.percentile(variation_scores, 75) - np.percentile(variation_scores, 25)

print(f"Mean variation score: {mean_var:.4f}")
print(f"Median variation score: {median_var:.4f}")
print(f"IQR: {iqr_var:.4f}")
Mean variation score: 0.7691
Median variation score: 0.8701
IQR: 1.2085

순서대로 평균, 중앙값, 사분위수. 저거 얼로 쏠렸구만...

 

# Define high-variation hotspots (top 10%)
threshold = np.percentile(variation_scores, 90)

hotspots = variation_scores[variation_scores >= threshold]
non_hotspots = variation_scores[variation_scores < threshold]

u_stat, p_value = mannwhitneyu(
    hotspots,
    non_hotspots,
    alternative="greater"
)

print(f"Hotspot threshold (90th percentile): {threshold:.4f}")
print(f"Mann–Whitney U statistic: {u_stat:.1f}")
print(f"p-value: {p_value:.4e}") # 아 이거는 제가 소수점 조절을 못했어요... 하면 큰일나...
Hotspot threshold (90th percentile): 1.5942
Mann–Whitney U statistic: 5708114.0
p-value: 0.0000e+00

그… 피밸류가 이게 맞아요…?

 

# 변이 점수 분포 히스토그램 (NanumSquare 적용)
plt.figure(figsize=(10, 6))
plt.hist(variation_scores, bins=50, color='#34495e', edgecolor='white', alpha=0.8)

# 통계 지표 수직선 표시
plt.axvline(mean_var, color='red', linestyle='dashed', linewidth=1, label=f'Mean: {mean_var:.4f}')
plt.axvline(median_var, color='orange', linestyle='dashed', linewidth=1, label=f'Median: {median_var:.4f}')

plt.title('라이노바이러스 변이 점수 분포 (Entropy Distribution)', fontsize=15)
plt.xlabel('Variation Score (Entropy)', fontsize=12)
plt.ylabel('Frequency', fontsize=12)
plt.legend()
plt.grid(axis='y', alpha=0.3)
plt.show()

음... 쏠렸어... 이븐하지 아니해...

 

Phylogenic tree

# 1. 거리 행렬 계산 (Identity 모델 사용)
calculator = DistanceCalculator('identity')
dm = calculator.get_distance(alignment)

# 2. Neighbor-Joining(NJ) 트리 생성
constructor = DistanceTreeConstructor(calculator, 'nj')
tree = constructor.build_tree(alignment)
tree.root_at_midpoint() # 루트를 중간으로 잡아 균형 잡힌 트리 생성

# 3. 시각화 (NanumSquare 폰트가 이미 글로벌 설정되어 있으므로 바로 출력!)
fig = plt.figure(figsize=(12, 8), dpi=100)
ax = fig.add_subplot(1, 1, 1)
plt.title("Rhinovirus Phylogenetic Tree (Based on Whole Genome)", fontsize=18, pad=20)

# Bio.Phylo를 이용한 트리 드로잉
Phylo.draw(tree, axes=ax, do_show=False, label_func=lambda n: str(n) if n.is_terminal() else "")

# 후처리: 축 숨기기 등 깔끔하게 정리
plt.axis('off')
plt.tight_layout()
plt.show()

나도 저거 이름으로 바꾸고 싶은데... 제미나이랑 같이 뭔 짓을 해봐도 안바껴...

 

def pairwise_identity(seq1, seq2):
    matches = sum(a == b for a, b in zip(seq1, seq2) if a != '-' and b != '-')
    length = sum(a != '-' and b != '-' for a, b in zip(seq1, seq2))
    return matches / length if length > 0 else 0

def extract_clades(tree, cutoff=0.05):
    clade_map = {}
    clade_id = 0

    for clade in tree.find_clades():
        if clade.branch_length and clade.branch_length > cutoff:
            terminals = clade.get_terminals()
            for t in terminals:
                clade_map[t.name] = f"Clade_{clade_id}"
            clade_id += 1

    return clade_map
clade_map = extract_clades(tree, cutoff=0.05)

# ID 정규화 (이거 중요)
normalized_clade_map = {}
for k, v in clade_map.items():
    normalized_clade_map[k.split('.')[0]] = v
within_clade = []
between_clade = []

for rec1, rec2 in combinations(alignment, 2):
    id1 = rec1.id.split('.')[0]
    id2 = rec2.id.split('.')[0]

    if id1 not in normalized_clade_map or id2 not in normalized_clade_map:
        continue

    identity = pairwise_identity(str(rec1.seq), str(rec2.seq))

    if normalized_clade_map[id1] == normalized_clade_map[id2]:
        within_clade.append(identity)
    else:
        between_clade.append(identity)

print(len(within_clade), len(between_clade))
u, p = mannwhitneyu(
    within_clade,
    between_clade,
    alternative="greater"
)

print(f'U-statistic: {u:.1f}')
print(f'p-value: {p:.4e}') # 그... 이게... 맞아요?

아 위 블록 안주냐고요? 저거 걍 길이라 의미 없음. 맨 휘트니나 보고 가십쇼.

 

U-statistic: 99900.0
p-value: 4.4644e-84

0은 아니구나… 허허…

 

effect_size = (
    np.median(within_clade) - np.median(between_clade)
)

print(f'effect_size: {effect_size:.4f}')
n1 = len(within_clade)
n2 = len(between_clade)

rbc = 1 - (2 * u) / (n1 * n2)
print(f"Rank-biserial r: {rbc:.4f}")
effect_size: 0.3865
Rank-biserial r: -0.9867

r 저거 20개 달았을때는 1떴는데?

반응형

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

M1V1 = M2V2  (0) 2026.02.15
코로나바이러스 MSA  (0) 2026.02.05
식물 데이터도 분석이 되나요?  (0) 2026.01.27
매우 주관적인 씨본 컬러맵 고르는 방법  (0) 2026.01.26
포켓몬과 이항분포  (0) 2026.01.21
Lv. 36 라이츄

Lv. 36 라이츄

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

식물 데이터도 분석이 되나요?

Coding/Python 2026. 1. 27. 09:00
반응형

결론부터 말하자면 분석은 된다. 되는데 데이터 리소스를 NCBI로 하는 건 비추다. 왜냐고? 그 이유는 올리면서 말해드림..


def plot_lht_family_tree(alignment_file):
    align = AlignIO.read(alignment_file, "fasta")
    calculator = DistanceCalculator('identity')
    constructor = DistanceTreeConstructor(calculator, 'nj')
    tree = constructor.build_tree(align)
    
    fig = plt.figure(figsize=(15, 8))
    ax = fig.add_subplot(1, 1, 1)
    plt.title("Arabidopsis LHT Family (1-10) Phylogeny (NCBI Data)", fontsize=15)
    
    # 연구자님이 설정한 전역 폰트(NanumSquare)가 적용됩니다.
    Phylo.draw(tree, axes=ax, do_show=False, label_func=lambda n: str(n) if n.is_terminal() else "")
    plt.tight_layout()
    plt.show()

plot_lht_family_tree("ARATH_LHT_aligned.aln")

얘가 NCBI에서 유전자 이름으로 찾은거다. 검색한 유전자 이름은 LHT(Lysine/Histidine Transporter) 1.

 

def plot_flexible_heatmap(alignment_file, target_id):
    """
    alignment_file: MSA 결과 파일 (.aln)
    target_id: 기준이 될 유전자 ID (예: 'LHT1' 또는 'At5g40780')
    """
    align = AlignIO.read(alignment_file, "fasta")
    
    # 1. 매개변수로 받은 target_id가 포함된 레코드 찾기
    target_idx = -1
    for i, rec in enumerate(align):
        if target_id in rec.id:
            target_idx = i
            break
            
    if target_idx == -1:
        print(f"❌ 오류: 파일 내에서 '{target_id}'를 찾을 수 없습니다.")
        return

    target_seq = str(align[target_idx].seq)
    names = [rec.id for rec in align]
    identities = []

    # 2. 기준 서열 vs 전체 서열 비교 (Gap-corrected)
    for rec in align:
        curr_seq = str(rec.seq)
        matches = sum(1 for a, b in zip(target_seq, curr_seq) if a == b and a != "-")
        valid_len = sum(1 for a, b in zip(target_seq, curr_seq) if a != "-" and b != "-")
        
        identity = (matches / valid_len * 100) if valid_len > 0 else 0
        identities.append(identity)

    # 3. 데이터프레임 및 시각화
    df = pd.DataFrame(identities, index=names, columns=[f'Standard: {target_id}'])
    df = df.sort_values(by=df.columns[0], ascending=False)

    plt.figure(figsize=(8, 10))
    sns.heatmap(df, annot=True, fmt=".1f", cmap="Blues")
    plt.title(f"Comparison based on {target_id}")
    plt.tight_layout()
    return df

plot_flexible_heatmap("ARATH_LHT_aligned.aln", "LHT1")

그리고 이건 LHT1과 딸린 식구들의 유사도 히트맵. 이것만 봐서는 뭐가 문제인지 모르시겠죠?

 

이게 내 논문에 있는 염색체 번호를 바탕으로 다시 그린거다.

 

많이 다르죠? 오른쪽 히트맵도 본인 논문에 서플로 들어갔던거랑은 좀 다른데 그 정도는 뭐 아 10년동안 연구자들이 규명해서 달라졌구나 할 수 있는 정도거든요? 근데 쟤는 너무 다른거야. 그래서 지피티한테 이게 왜 다른지 물어봤는데, 식물 유전자로 분석할거면 NCBI 말고 식물 전용 DB를 털라고 하더라고. 애기장대면 TAIR라고 있으니까 거기 터십쇼.

 

[염색체 번호 기준 신원 확인 결과]
Paper_Name  Locus_ID NCBI_Original_Name Match_Status
      LHT1 AT5G40780     ❌ NCBI 리스트에 없음       데이터 누락
      LHT2 AT1G24400     ❌ NCBI 리스트에 없음       데이터 누락
      LHT3 AT1G61270     ❌ NCBI 리스트에 없음       데이터 누락
      LHT4 AT1G47670     ❌ NCBI 리스트에 없음       데이터 누락
      LHT5 AT1G67640     ❌ NCBI 리스트에 없음       데이터 누락
      LHT6 AT3G01760     ❌ NCBI 리스트에 없음       데이터 누락
      LHT7 AT4G36180     ❌ NCBI 리스트에 없음       데이터 누락
      LHT8 AT1G71680     ❌ NCBI 리스트에 없음       데이터 누락
      LHT9 AT1G48640     ❌ NCBI 리스트에 없음       데이터 누락
     LHT10 AT1G25530     ❌ NCBI 리스트에 없음       데이터 누락

봐봐요 염색체로 찾으니까 안나오지.

반응형
Lv. 36 라이츄

Lv. 36 라이츄

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

매우 주관적인 씨본 컬러맵 고르는 방법

Coding/Python 2026. 1. 26. 23:04
반응형

https://seaborn.pydata.org/tutorial/color_palettes.html

 

Choosing color palettes — seaborn 0.13.2 documentation

Choosing color palettes Seaborn makes it easy to use colors that are well-suited to the characteristics of your data and your visualization goals. This chapter discusses both the general principles that should guide your choices and the tools in seaborn th

seaborn.pydata.org

여기 들어가보면 아주 온갖가지 컬러팔레트가 다 있다. 그거랑 별개로 본인이 직접 만들 수도 있는데 이것도 나중에 설명해드림… 오늘은 여기서 제공해주는 것만 봅시다.


평범한 그래프

다채로운거 좋아하시면 tab10, tab20이 있다. 배리에이션으로 브라잇 다크 이런것도 있으니까 그런거 쓰십쇼. set2도 좋음. 내가 써보니까 Blues, Grays같은 단색은 막대그래프나 꺾은선그래프에서 색 변화도 좀 단조롭고 구별하기가 많이 빡셈. 그리고 단색 컬러맵은 시발점이 연한색이라 막대그래프나 꺾은선그래프에 뭐 얼마 없는데 화이트그리드 주잖아요? 주사터널현미경 예약해야하나 싶게 선이고 막대고 안보임. _r 주면 반대로 그리는거라 색이 진해지긴 한데, 그럼에도 불구하고 막대나 선끼리 구별 안 되는 게 단점이다.

 

히트맵

근데 막대그래프 그릴때는 눈뽕 오지게 오는 Blues나 Grays같은 단색 라인이 오히려 히트맵 그릴 때는 좋다. 히트맵은 그 뭐라해야되지? 색의 밝기? 명도? 그런걸로 구별하는거거든요. 그게 약간 큐알코드마냥 생긴 그래프인데 찐할수록 높은거고 연할수록 낮은거고 그래요. 근데 그런거에 set2나 tab10 쓴다? 눈뽕은 둘째치고 이게 뭐가 뭔지 구별이 안 된다. 히트맵 그릴때는 단색 파레트나 YlOrBr같은 거 쓰세요. 색깔에 따라 농도 구별이 명확해야 하는 그래프면 적어도 단색이나 밝은색->어두운색으로 가는 파레트로 쓰는 게 좋다. 마그마나 viridis도 괜찮았음.

 

양 끝단에 다른 색이 있는 파레트

본인은 쿨웜이나 아이스파이어 주로 쓰는 편인데, 어떤거 말하는거냐면

이거(밑에 있는게 아이스파이어) 그리고

 

이거(밑에 있는게 쿨웜). 이런 파레트들이 약간 반전요소? 그런거 넣기는 좋다. 예를 들어서 빨간색은 증가, 파란색은 감소 이런거. 근데 히트맵에서 쓰기는 좀 번잡시럽고 쿨웜같은 경우 다크그리드랑 병행하면 막대가 잘 안보임.

 

그 외에도 히트맵에 cmap 안 주면 기본으로 걸리는 마그마나 viridis도 좋은 선택이다. 저 두개는 히트맵에 써도 이쁨. 

반응형
Lv. 36 라이츄

Lv. 36 라이츄

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

포켓몬과 이항분포

Coding/Python 2026. 1. 21. 20:40
반응형

https://koreanraichu.tistory.com/760

 

포켓몬 이로치가 나올 확률로 이항분포를 때려보자

일단 이항분포가 뭐냐… 특정 확률(p)을 가진 베르누이 시행을 n번 독립적으로 반복했을 때, 성공하는 횟수(X)에 대한 이산 확률 분포라고 한다. Pass or Fail 뭐 이런건데, 여기서 중요한 건 결과가

koreanraichu.tistory.com

이거 파이썬으로 하는거 맞음.


성비와 이항분포

포켓몬에는 성비가 존재한다. 성비가 불명(무성)이거나 한쪽으로 쏠려있는 경우도 있지만 보통은 암수가 다 있는데 비율이 다른 경우가 많다. 님들 솔직히 세꿀버리 비퀸 만들어야되는데 암컷 안떠서 피눈물 흘리신 적 있으시죠? 난 있음... 아오 포고 야도뇽이 자꾸 수컷만 나와서 대환장파티여...

 

# p(성비): 7:1(0.875:0.125) / 1:1(0.5:0.5)
# n(트라이 수) = 3000 (100박스)

n = 3000
p1 = 0.125 # 암컷
p2 = 0.5

# 성비별 난수
binom_uneven = np.random.binomial(1, p1, n) # 알은 한번에 하나 나옵니다
binom_even = np.random.binomial(1, p2, n)

# 기댓값 n * p
np_uneven = n * p1
np_even = n * p2

display(f'7:1 평균 {np.mean(binom_uneven):.4f}, 기댓값 {np_uneven}')
display(f'5:5 평균 {np.mean(binom_even):.4f}, 기댓값 {np_even}')
'7:1 평균 0.1167, 기댓값 375.0'
'5:5 평균 0.5017, 기댓값 1500.0'

그 우리가... 알이... 한번에 하나나와... 그래서 저 코드가 뭔데요? 각 성비에 따라 암컷 포켓몬을 뽑기 위해서 3000번 시물 돌린 평균과 기대값이다. 성비가 언이븐한쪽이 좀 더 낮죠? 저 언이븐한쪽이 7:1이다. 일단 둘 다 암컷을 뽑는 걸로 전제하고 돌린게 이거다.

 

# 왔구나 이항분포오오오
# 성공횟수: 0~10
# 트라이: 10
# 확률: 0.125

for k in range(11):
    p = stats.binom.pmf(k, 10, 0.125)
    print(f'10번 트라이해서 {k}마리의 암컷 포켓몬을 얻을 확률: {p:.5f}')
10번 트라이해서 0마리의 암컷 포켓몬을 얻을 확률: 0.26308
10번 트라이해서 1마리의 암컷 포켓몬을 얻을 확률: 0.37582
10번 트라이해서 2마리의 암컷 포켓몬을 얻을 확률: 0.24160
10번 트라이해서 3마리의 암컷 포켓몬을 얻을 확률: 0.09204
10번 트라이해서 4마리의 암컷 포켓몬을 얻을 확률: 0.02301
10번 트라이해서 5마리의 암컷 포켓몬을 얻을 확률: 0.00394
10번 트라이해서 6마리의 암컷 포켓몬을 얻을 확률: 0.00047
10번 트라이해서 7마리의 암컷 포켓몬을 얻을 확률: 0.00004
10번 트라이해서 8마리의 암컷 포켓몬을 얻을 확률: 0.00000
10번 트라이해서 9마리의 암컷 포켓몬을 얻을 확률: 0.00000
10번 트라이해서 10마리의 암컷 포켓몬을 얻을 확률: 0.00000

10번 해서 이브이 암컷 한마리 뽑을 확률이 0.37...이니까 30%라는 얘기다. 힘내십셔.

 

# 왔구나 이항분포오오오
# 성공횟수: 0~10
# 트라이: 10
# 확률: 0.5

for k in range(11):
    p = stats.binom.pmf(k, 10, 0.5)
    print(f'10번 트라이해서 {k}마리의 암컷 포켓몬을 얻을 확률: {p:.5f}')
10번 트라이해서 0마리의 암컷 포켓몬을 얻을 확률: 0.00098
10번 트라이해서 1마리의 암컷 포켓몬을 얻을 확률: 0.00977
10번 트라이해서 2마리의 암컷 포켓몬을 얻을 확률: 0.04395
10번 트라이해서 3마리의 암컷 포켓몬을 얻을 확률: 0.11719
10번 트라이해서 4마리의 암컷 포켓몬을 얻을 확률: 0.20508
10번 트라이해서 5마리의 암컷 포켓몬을 얻을 확률: 0.24609
10번 트라이해서 6마리의 암컷 포켓몬을 얻을 확률: 0.20508
10번 트라이해서 7마리의 암컷 포켓몬을 얻을 확률: 0.11719
10번 트라이해서 8마리의 암컷 포켓몬을 얻을 확률: 0.04395
10번 트라이해서 9마리의 암컷 포켓몬을 얻을 확률: 0.00977
10번 트라이해서 10마리의 암컷 포켓몬을 얻을 확률: 0.00098

저거 시각화해보니까 약간 정규분포같이 생겼던데? 아무튼 1:1일때는 반반 확률이 제일 높다.

 

이로치와 이항분포

그 위에 게시글에서 1/4096, 1/1365, 1/512로 했었죠? 그거 할 거다.

 

1/4096

# 확률: 1/4096
# 트라이: 3000
p = 1 / 4096

for k in range(11):
    shiny = stats.binom.pmf(k, 3000, p)
    print(f'3000번 트라이해서 {k}마리의 이로치를 만날 확률: {shiny:.5f}')
3000번 트라이해서 0마리의 이로치를 만날 확률: 0.48070
3000번 트라이해서 1마리의 이로치를 만날 확률: 0.35216
3000번 트라이해서 2마리의 이로치를 만날 확률: 0.12895
3000번 트라이해서 3마리의 이로치를 만날 확률: 0.03147
3000번 트라이해서 4마리의 이로치를 만날 확률: 0.00576
3000번 트라이해서 5마리의 이로치를 만날 확률: 0.00084
3000번 트라이해서 6마리의 이로치를 만날 확률: 0.00010
3000번 트라이해서 7마리의 이로치를 만날 확률: 0.00001
3000번 트라이해서 8마리의 이로치를 만날 확률: 0.00000
3000번 트라이해서 9마리의 이로치를 만날 확률: 0.00000
3000번 트라이해서 10마리의 이로치를 만날 확률: 0.00000

그 된장맛 식스테일은 대체 어떻게 뜬 거지? 아니 예전에 소드실드 도감작하다가 식스테일 이로치 하나 잡았거든요.

 

1/1365

# 확률: 1/1365 (빛부)
# 트라이: 3000
p = 1 / 1365

for k in range(11):
    shiny = stats.binom.pmf(k, 3000, p)
    print(f'3000번 트라이해서 {k}마리의 이로치를 만날 확률: {shiny:.5f}')
3000번 트라이해서 0마리의 이로치를 만날 확률: 0.11096
3000번 트라이해서 1마리의 이로치를 만날 확률: 0.24404
3000번 트라이해서 2마리의 이로치를 만날 확률: 0.26828
3000번 트라이해서 3마리의 이로치를 만날 확률: 0.19656
3000번 트라이해서 4마리의 이로치를 만날 확률: 0.10797
3000번 트라이해서 5마리의 이로치를 만날 확률: 0.04743
3000번 트라이해서 6마리의 이로치를 만날 확률: 0.01736
3000번 트라이해서 7마리의 이로치를 만날 확률: 0.00544
3000번 트라이해서 8마리의 이로치를 만날 확률: 0.00149
3000번 트라이해서 9마리의 이로치를 만날 확률: 0.00036
3000번 트라이해서 10마리의 이로치를 만날 확률: 0.00008

이건 빛나는부적+야생이라고 보시면 된다.

 

1/512

# 확률: 1/512 (빛부+국제교배)
# 트라이: 3000
p = 1 / 512

for k in range(11):
    shiny = stats.binom.pmf(k, 3000, p)
    print(f'3000번 트라이해서 {k}마리의 이로치를 만날 확률: {shiny:.5f}')
3000번 트라이해서 0마리의 이로치를 만날 확률: 0.00284
3000번 트라이해서 1마리의 이로치를 만날 확률: 0.01665
3000번 트라이해서 2마리의 이로치를 만날 확률: 0.04887
3000번 트라이해서 3마리의 이로치를 만날 확률: 0.09557
3000번 트라이해서 4마리의 이로치를 만날 확률: 0.14013
3000번 트라이해서 5마리의 이로치를 만날 확률: 0.16432
3000번 트라이해서 6마리의 이로치를 만날 확률: 0.16051
3000번 트라이해서 7마리의 이로치를 만날 확률: 0.13435
3000번 트라이해서 8마리의 이로치를 만날 확률: 0.09837
3000번 트라이해서 9마리의 이로치를 만날 확률: 0.06399
3000번 트라이해서 10마리의 이로치를 만날 확률: 0.03746

혹시 이거 보면서 왜 하필 3000트일지 궁금하지 않았습니까? 포켓몬 인게임 내에는 박스가 있는데… 박스 하나에 포켓몬이 30마리 들어갑니다. 포켓몬 홈이 6000마리까지 되잖아요? 박스가 200개 있습니다. 그래서 박스 단위 끊는다고 3000트가 된 거다. 3000마리면 100박스임.

 

그리고 한가지 더 알아두셔야 하는 게 있는데, 1/4096이나 1/1365와 달리 1/512는 알까기 트라이 수이다. 전제에 국제교배가 붙어있는데 그게 '어버이 국적이 다른 두 포켓몬'을 교배시켜서 얻는거라 가끔 외국 유저랑 6V 메타몽을 바꾸기도 한다. 나는 한국어니까 한국어가 아닌 메타몽(혹은 한국어가 아닌 포켓몬)과 교배를 해야 저 방법이 성립하거든.

 

# 데이터프레임이 요기잉눼?
p_s1 = 1/4096
p_s2 = 1/1365
p_s3 = 1/512

k = list(range(11))

shiny1 = stats.binom.pmf(k, 3000, p_s1)
shiny2 = stats.binom.pmf(k, 3000, p_s2)
shiny3 = stats.binom.pmf(k, 3000, p_s3)

shiny_d = {
    'shiny_encounter': k,
    "1/4096": shiny1,
    "1/1365": shiny2,
    "1/512": shiny3
}

shiny_df = pd.DataFrame(shiny_d)
shiny_df
shiny_df.plot(x='shiny_encounter', style=['-', '--', ':'])
plt.title('Shiny pokemon encounter', fontsize = 16)
plt.xlabel('Shiny encounter')
plt.ylabel('Binom')
plt.show()

스토리를 함께 해 온 포켓몬들도 소중하지만, 빛부가 없는 상태에서 만난 이로치는 더 소중합니다 여러분.

반응형
Lv. 36 라이츄

Lv. 36 라이츄

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

캐글 EDA-마! 서퍼티파이! (2)

Coding/EDA 2026. 1. 20. 20:20
반응형

https://koreanraichu.tistory.com/839

 

캐글 EDA-마! 서퍼티파이!

https://www.kaggle.com/datasets/serkantysz/550k-spotify-songs-audio-lyrics-and-genres/data 550K Spotify Songs: Audio, Lyrics & GenresEnhanced Music Dataset with Audio Features, Lyrics, Genres & Artist Metadatawww.kaggle.com참고로 본인은 스포티파

koreanraichu.tistory.com

우리 어제 전처리까지 하고 끝냈음… 기억하시죠? 하다하다 VScode가 뻗었다고…


아티스트 분석

여기는 뭐 없어서 분량도 짧다.

 

# 1그룹에 다 몰렸구나... 
# 결측값을 0으로 채움
genre_follower = artist_df.groupby(['main_genre','follower_group'], observed=True)['id'].size().unstack()

# 각 장르별 합계
genre_follower['total_group'] = genre_follower.sum(axis=1)

# 정렬
genre_sort = genre_follower.sort_values('total_group', ascending=False)
genre_sort

그… 우리 어제 cut으로 나눈거 기억하시죠? 근데 판다스에는 걍 컷 말고 큐컷이 있다. 이건 또 뭔데요? 컷은 우리가 기준을 정하는거고, 큐컷은 알아서 n등분 하쇼~ 하면 판다스가 나눠준다.

 

sns.barplot(genre_sort, x = 'main_genre', y = 'total_group', hue = 'main_genre')
plt.xlabel('장르')
plt.ylabel('아티스트')
plt.show()

일단 장르별 팔로워 수는 일렉트로닉이랑 락이 많구만... 인기도도 또이또이다.

 

# 장르별, 팔로우 그룹별 시각화
genre_follower_hist = artist_df.groupby(['main_genre','follower_group'], observed=True)['id'].size().unstack()

# 정규화
genre_norm = genre_follower_hist.div(genre_follower_hist.sum(axis=1), axis=0)

# 시각화
genre_norm.plot(kind='barh', stacked=True, figsize=(12, 8), colormap='viridis')

plt.title('장르별 인기도 그룹 비중 (정규화)', fontsize=15)
plt.xlabel('비중 (Percentage)')
plt.ylabel('주요 장르')
plt.legend(title='인기도 그룹', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()

팔로워 수에 차이가 있으니까 정규화해서 비중을 봐야 한다. 그 포켓몬중에 지가르데라고 있는데, 얘가 퍼펙트폼으로 폼체인지를 할 때 피 회복이 되거든요? 신기하지 않습니까? 폼체인지를 하는데 피가 회복이 돼.

 

사실은 피 회복이 되는 게 아니라, HP 종족값이 늘어나서 최대 피통이 바뀐 것 때문에 체력이 회복되는 것처럼 보이는 것이다. 쉽게 말하자면 체력이 100인데 60을 잃으면 60%를 잃는거죠? 근데 체력이 200인데 60 잃은건 30%잖아요. 그런거임. 만 명 있는데 100명인거랑 1000명 있는데 100명 있는거랑은 엄연히 갭이 있기 때문에 정규화를 하는거라고 보시면 된다.

 

# 팔로워 상위 10%
top_10_percent = artist_df[artist_df['follower_group'] == 10]

top_10_genre = top_10_percent.groupby('main_genre').size()
top_10_genre_df = top_10_genre_s.reset_index()
top_10_genre_df.columns = [*top_10_genre_df.columns[:-1], 'artist_count']
top_10_genre_s = top_10_genre_df.sort_values('artist_count')

sns.barplot(data=top_10_genre_df, x='main_genre', y='artist_count', hue='main_genre', legend=False)
plt.xlabel('장르')
plt.ylabel('아티스트 수')
plt.show()

아, 참고로 테일러 스위프트도 팝에 있습니다. 시런좌도 있음. 스바 엔딩 크레딧곡 셀레스티얼 들어보십쇼. 

 

노래 분석

그 우리 노래 연도 10틱으로 나눈거 있죠? 연도별로도 나눴음.

# 년도가 10년단위라서... 임시로 갑니다. 김람다씨! 
song_df['Era_year'] = song_df['year'].apply(lambda x: f"{(int(x) // 10) * 10}s")

그니까 1990년에 발매된 노래나 1980년에 발매된 노래나 다 1900s로 갑니다.

 

song_df.groupby(['Era_year','Era']).size().unstack()

세로로 길어서 언스택했더니 무슨 거리행렬마냥 돼버렸네.

 

봐봐요. 거리행렬이지. 주대각선에만 있어. …근데 이거 100년 기준인데 왜 이렇게 된 거임?

 

# 년도가 10년단위라서... 임시로 갑니다. 김람다씨! 
song_df['Era_year'] = song_df['year'].apply(lambda x: f"{(int(x) // 100) * 100}s")

이 에미나이 이거 찰떡같이 말해도 개떡같이 알아듣는구만기래.

 

의도는 이거였음.

 

시대와 시대

plt.figure(figsize=(10, 6))
# 1900s vs 2000s 인기도 분포 비교
sns.boxplot(data=song_df, x='Era_year', y='popularity', hue='Era_year', palette='Set2', legend=False)

plt.title('20세기 vs 21세기 곡 인기도 분포 비교', fontsize=16)
plt.show()

최신곡이라고 무조건 인기가 있는 건 아니기에… 뭐 요즘 레트로 열풍 이런것도 불고 있잖습니까. 근데… 솔직히 저 이상치 뭔지 궁금하지 않아요?

 

song_df.query('popularity > 80 and Era_year == "1900s"').sort_values('popularity', ascending=False).head()

솔직히 머라이어 캐리는 인정해야죠들. 우리나라에는 장범준의 벚꽃연금이 있다면 캐리언니는 크리스마스 연금 받는 분이심. 크리스마스마다 해동하잖음 ㅇㅇ

 

song_df.query('popularity > 80 and Era_year == "2000s"').sort_values('popularity', ascending=False).head()

역쉬 캐롤이 1위다. 근데 캐리누님이 근소하게 인기도는 더 높음. 아 얼려놨다가 겨울마다 해동한다니깐 그냥반은.

 

1990년대 띵곡들

song_1990 = song_df.query('Era_year == "1900s"')

era_boss_songs = song_1990.loc[song_1990.groupby('Era', observed=True)['popularity'].idxmax()]
display(era_boss_songs[['Era', 'artists', 'name', 'popularity']].sort_values('Era'))

진짜 궁금해서 물어보는건데… 50년대 이전 노래는 어떻게 찾은거임?

 

plt.figure(figsize=(12, 8))
# 20세기 곡들의 댄스빌리티와 에너지 관계도
sns.scatterplot(data=song_1990, x='danceability', y='energy', hue='popularity', size='popularity', alpha=0.5, sizes=(20, 200))

# 인기도 상위 5개 곡에 이름표 달아주기
for i in range(5):
    target = era_boss_songs.iloc[i]
    plt.text(target['danceability']+0.01, target['energy']+0.01, target['name'], 
                fontsize=10, weight='bold')

plt.title('20세기 띵곡들의 오디오 특징 분포', fontsize=16)
plt.show()

이거 저기 친구네 실험실 가서 현미경 빌려와야것는디?

 

# 인기도 80 이상만 명확하게 표시
top_classics = song_1990[song_1990['popularity'] >= 80]

plt.figure(figsize=(12, 8))
# 전체 배경은 연하게
sns.scatterplot(data=song_1990, x='danceability', y='energy', color='lightgrey', alpha=0.1)
# 띵곡들만 진하게
sns.scatterplot(data=top_classics, x='danceability', y='energy', 
                hue='popularity', size='popularity', palette='rocket', sizes=(50, 300))
plt.title('1990년대의 띵곡')
plt.show()

연한 놈이 범인이여. 개인적으로 이런 그래프는 마그마나 비리디스보단 블루스, 그레이스같은 단색이 더 무난한 것 같다.

 

보라색이 있었으면 말을 하지 그래... 아무튼 여러분들은 히트맵이나 이런 산점도 그릴때 가급적 단색 쓰세요. 극단적인 변화량을 보여주고 싶으면 쿨웜이나 아이스파이어도 좋긴 한데 색깔 여러개면 눈뽕옴.

 

이건 KDE 등고선 추가 버전. 이걸 2000년대 곡들에도 할거다.

 

2000년대 띵곡들

song_2000 = song_df.query('Era_year == "2000s"')

era_boss_songs = song_2000.loc[song_2000.groupby('Era', observed=True)['popularity'].idxmax()]
display(era_boss_songs[['Era', 'artists', 'name', 'popularity']].sort_values('Era'))

스포티파이는 빨리 일본곡 차트를 내놓아라. (다 모름)

 

# 인기도 80 이상만 명확하게 표시
top_classics = song_2000[song_2000['popularity'] >= 80]

plt.figure(figsize=(12, 8))
# KDE Plot (밀도 등고선) 추가
sns.kdeplot(data=song_2000, x='danceability', y='energy', levels=5, color="black", linewidths=1)
# 전체 배경은 연하게
sns.scatterplot(data=song_2000, x='danceability', y='energy', color='lightgrey', alpha=0.1)
# 띵곡들만 진하게
sns.scatterplot(data=top_classics, x='danceability', y='energy', 
                hue='popularity', size='popularity', palette='Purples', sizes=(50, 300))
plt.title('2000년대의 띵곡')
plt.show()

2000년대는 좀 더 등고선이 위로 올라간 느낌이다. 근데 이거 왜케 오래걸림?

 

아티스트별 띵곡 찾기

마이클 잭슨

# 구조된 마이클 잭슨 곡의 '민낯' 공개
mj_the_one = song_df[song_df['artists'].str.contains('Michael Jackson', case=False, na=False)].sort_values('popularity', ascending=False)   

# 데이터 수치 확인 (NanumSquare 폰트로 출력!)
display(mj_the_one[['name', 'year', 'popularity', 'danceability', 'energy', 'valence']][0:5])

아 나도 마잭은 알아요 이 사람들아.

 

에드시런

# 에드시런 찾아 삼만리
mj_the_one = song_df[song_df['artists'].str.contains('Ed Sheeran', case=False, na=False)].sort_values('popularity', ascending=False)   

# 데이터 수치 확인 (NanumSquare 폰트로 출력!)
display(mj_the_one[['name', 'year', 'popularity', 'danceability', 'energy', 'valence']][0:5])

셀레스티얼은 밀렸군… 근데 shape of you도 좋음. 셀레스티얼은 스칼렛/바이올렛 엔딩 크레딧이기도 하고, 이 곡이 본가에 최초로 삽입된 '보컬 있는' 곡입니다. 기적은 가사는 있는데 보컬이 없음.

 

BTS

# 방탄 월클 맞다
mj_the_one = song_df[song_df['artists'].str.contains('BTS', case=False, na=False)].sort_values('popularity', ascending=False)   

# 데이터 수치 확인 (NanumSquare 폰트로 출력!)
display(mj_the_one[['name', 'year', 'popularity', 'danceability', 'energy', 'valence']][0:5])

어… 저는 아미는 아닌데요… 걍 있나 해서 쳐봤더니 나왔어요… 다이너마이트 좋음. 라잇 업 라잌 라이크 다이나마잇 오오오

 

반응형

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

Post-COVID Video Games Worldwide (2021-2025)  (0) 2026.02.10
또 ChEMBL을 털어보았다  (0) 2026.01.28
캐글 EDA-마! 서퍼티파이!  (0) 2026.01.19
그냥 해보는 ChEMBL EDA  (0) 2026.01.15
캐글 EDA-Video game sales  (0) 2026.01.09
Lv. 36 라이츄

Lv. 36 라이츄

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

캐글 EDA-마! 서퍼티파이!

Coding/EDA 2026. 1. 19. 23:26
반응형

https://www.kaggle.com/datasets/serkantysz/550k-spotify-songs-audio-lyrics-and-genres/data

 

550K Spotify Songs: Audio, Lyrics & Genres

Enhanced Music Dataset with Audio Features, Lyrics, Genres & Artist Metadata

www.kaggle.com

참고로 본인은 스포티파이 계정이 있습니다. 왜냐고? 포슬립 사운드트랙이 거기 있으니까...


import kagglehub

# Download latest version
path = kagglehub.dataset_download("serkantysz/550k-spotify-songs-audio-lyrics-and-genres")

print("Path to dataset files:", path)

저 캐글허브 pip로 설치하시면 다운로드 안 받아도 가져올 수 있습니다.

 

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# 그래프 기본 테마 설정
sns.set_theme(palette="icefire", style="darkgrid", font_scale=1)
sns.color_palette("icefire", as_cmap=True)

# 그래프를 그리기 위한 기본 설정
plt.rcParams['font.family'] = '그리운 겔리롤'
# plt.rcParams['font.family'] = 'AppleGothic'
plt.rcParams['figure.figsize'] = 12, 9
plt.rcParams['font.size'] = 16
plt.rcParams['axes.unicode_minus'] = False

저 다운로드가 생각보다 오래 걸리니까 다운받을동안 세팅하십쇼. 다 되면 경로가 나올건데 그 경로 안에 보면 csv파일 있으니까 열면 된다. 거기 다른 사람이 EDA 올려둔거 참고해도 됨.

 

# 파일이 두개더군요... 
artist_df = pd.read_csv('/home/koreanraichu/.cache/kagglehub/datasets/serkantysz/550k-spotify-songs-audio-lyrics-and-genres/versions/1/artists.csv') 
song_df = pd.read_csv('/home/koreanraichu/.cache/kagglehub/datasets/serkantysz/550k-spotify-songs-audio-lyrics-and-genres/versions/1/songs.csv')

아 이거 갖다 쓰면 안되냐고요? 본인이 우분투 쓰고 계시면 그러시든지.


데이터 정보 확인

이거 파일 두개 다 볼거임.

 

.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 71440 entries, 0 to 71439
Data columns (total 6 columns):
 #   Column      Non-Null Count  Dtype 
---  ------      --------------  ----- 
 0   id          71440 non-null  object
 1   name        71438 non-null  object
 2   followers   71440 non-null  int64 
 3   popularity  71440 non-null  int64 
 4   genres      71440 non-null  object
 5   main_genre  71440 non-null  object
dtypes: int64(2), object(4)
memory usage: 3.3+ MB

아 뭐야 결측값 있어…

 

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 550622 entries, 0 to 550621
Data columns (total 24 columns):
 #   Column                  Non-Null Count   Dtype  
---  ------                  --------------   -----  
 0   id                      550622 non-null  object 
 1   name                    550619 non-null  object 
 2   album_name              550602 non-null  object 
 3   artists                 550622 non-null  object 
 4   danceability            550622 non-null  float64
 5   energy                  550622 non-null  float64
 6   key                     550622 non-null  int64  
 7   loudness                550622 non-null  float64
 8   mode                    550622 non-null  int64  
 9   speechiness             550622 non-null  float64
 10  acousticness            550622 non-null  float64
 11  instrumentalness        550622 non-null  float64
 12  liveness                550622 non-null  float64
 13  valence                 550622 non-null  float64
 14  tempo                   550622 non-null  float64
 15  duration_ms             550622 non-null  int64  
 16  lyrics                  550622 non-null  object 
 17  year                    550622 non-null  int64  
 18  genre                   550622 non-null  object 
 19  popularity              550622 non-null  int64  
 20  total_artist_followers  550622 non-null  int64  
 21  avg_artist_popularity   550622 non-null  float64
 22  artist_ids              550622 non-null  object 
 23  niche_genres            550622 non-null  object 
dtypes: float64(10), int64(6), object(8)
memory usage: 100.8+ MB

어쩐지 여는데 드럽게 오래 걸리더라고…

 

.describe()

followers popularity
count 7.144000e+04 71440.000000
mean 2.330490e+05 28.773138
std 2.204255e+06 18.006693
min 0.000000e+00 0.000000
25% 9.127500e+02 15.000000
50% 7.865000e+03 28.000000
75% 4.510750e+04 41.000000
max 1.685087e+08 100.000000
	id	name	genres	main_genre
count	71440	71438	71440	71440
unique	71440	69643	16134	10
top	7yhRUp1m94EmlkPRw7VoVQ	Chris Martin	[]	Electronic
freq	1	6	19580	20032

저 아이디는 PK인거임?

 

danceability	energy	key	loudness	mode	speechiness	acousticness	instrumentalness	liveness	valence	tempo	duration_ms	year	popularity	total_artist_followers	avg_artist_popularity
count	550622.000000	550622.000000	550622.000000	550622.000000	550622.000000	550622.000000	550622.000000	550622.000000	550622.000000	550622.000000	550622.000000	5.506220e+05	550622.000000	550622.000000	5.506220e+05	550622.000000
mean	0.527173	0.671389	5.275487	-7.877679	0.667360	0.085574	0.243174	0.107573	0.224043	0.464568	122.815094	2.374116e+05	2007.144742	17.575954	2.533411e+06	48.076930
std	0.172603	0.245591	3.558157	3.858865	0.471159	0.093337	0.307418	0.239932	0.196800	0.249094	29.453634	9.555273e+04	13.575992	17.457960	9.227678e+06	19.154401
min	0.045500	0.000020	0.000000	-44.868000	0.000000	0.021900	0.000000	0.000000	0.006730	0.000000	30.946000	1.502700e+04	1900.000000	0.000000	0.000000e+00	0.000000
25%	0.408000	0.496000	2.000000	-9.907000	0.000000	0.035000	0.003650	0.000000	0.099600	0.259000	99.978000	1.840400e+05	2002.000000	0.000000	5.510225e+04	35.000000
50%	0.530000	0.716000	5.000000	-7.038000	1.000000	0.049700	0.075500	0.000185	0.141000	0.447000	121.729000	2.230000e+05	2010.000000	14.000000	2.693830e+05	49.000000
75%	0.651000	0.886000	8.000000	-5.095000	1.000000	0.092200	0.432000	0.032200	0.294000	0.660000	140.938000	2.707830e+05	2017.000000	30.000000	1.354562e+06	62.000000
max	0.988000	1.000000	11.000000	0.000000	1.000000	0.966000	0.996000	0.998000	1.000000	0.998000	245.941000	4.995315e+06	2025.000000	98.000000	2.951819e+08	100.000000
id	name	album_name	artists	lyrics	genre	artist_ids	niche_genres
count	550622	550619	550602	550622	550622	550622	550622	550622
unique	550622	351146	140154	95121	478160	10	95661	23638
top	036JzAN5DCANSZyeW6MjqG	Home	Greatest Hits	["Grateful Dead"]	Who's gonna tell you when\n It's too late?\n ♪...	Rock	["4TMHGUX5WI7OOm53PqSDAT"]	[]
freq	1	221	1958	1500	56	197168	1500	6099

 

 

.isna().sum()

id            0
name          2
followers     0
popularity    0
genres        0
main_genre    0
dtype: int64
id                         0
name                       3
album_name                20
artists                    0
danceability               0
energy                     0
key                        0
loudness                   0
mode                       0
speechiness                0
acousticness               0
instrumentalness           0
liveness                   0
valence                    0
tempo                      0
duration_ms                0
lyrics                     0
year                       0
genre                      0
popularity                 0
total_artist_followers     0
avg_artist_popularity      0
artist_ids                 0
niche_genres               0
dtype: int64

이거 걍 언노운으로 때우면 안되나? 이따 함 보긴 하겠지만.

 

.head()

id	name	followers	popularity	genres	main_genre
0	6YROFUbu5zRCHi2xkir5pk	Brian Hyland	67223	47	[]	Pop
1	5tFRohaO5yEsuJxmMnlCO9	Barns Courtney	602647	62	[]	Electronic
2	3w1Q754jb31h5CXQCcnLNL	Capcom Sound Team	210392	58	['japanese vgm', 'soundtrack']	Electronic
3	3oDbviiivRWhXwIE8hxkVV	The Beach Boys	5139194	76	['baroque pop']	Classical
4	60zvRmhQHRxokEB1taAVpN	Beth Malone	1569	29	['musicals']	Classical
id	name	album_name	artists	danceability	energy	key	loudness	mode	speechiness	...	tempo	duration_ms	lyrics	year	genre	popularity	total_artist_followers	avg_artist_popularity	artist_ids	niche_genres
0	0Prct5TDjAnEgIqbxcldY9	!	UNDEN!ABLE	["HELLYEAH"]	0.415	0.605	7	-11.157	1	0.0575	...	100.059	79500	He said he came from Jamaica,\nhe owned a coup...	2016	Rock	0	769490	52.0	["4hxDvVq5t8ebPYPdBl1F9f"]	["groove metal", "metal"]
1	2ASl4wirkeYm3OWZxXKYuq	!!	Childhood Dreams	["Yxngxr1"]	0.788	0.648	7	-9.135	0	0.3150	...	79.998	114000	Fuck the bitch, now she running with my kids\n...	2019	Hip-Hop	29	143628	45.0	["2jwRHcdgkRhelYEMqndDKe"]	[]
2	5tA3ImW310llKo8EMBj2Ga	!!Noble Stabbings!!	Situationist Comedy	["Dillinger Four"]	0.171	0.957	2	-5.749	1	0.1490	...	175.317	197400	You like to stand on the other side\nPoint and...	2002	Rock	0	36619	35.0	["4YAN46l70QV0PGXlMg0iHi"]	["melodic hardcore", "pop punk", "punk", "skat...
3	0fROT4kK5oTm8xO8PX6EJF	!I'll Be Back!	!I'll Be Back!	["Ril\u00e8s"]	0.823	0.612	1	-7.767	1	0.2480	...	142.959	178533	It's been a while, shit, I missed the rehab, p...	2018	Hip-Hop	43	929303	63.0	["6pdcQa7by8IKuoVXvgknlI"]	["french rap"]
4	1xBFhv5faebv3mmwxx7DnS	!Lost!	!Lost!	["Ril\u00e8s"]	0.729	0.552	7	-8.562	0	0.0650	...	86.103	186197	I would like to give you all my time\nI would ...	2018	Hip-Hop	0	929303	63.0	["6pdcQa7by8IKuoVXvgknlI"]	["french rap"]

근데 캡콤은… 저기 뭘로 올라간거임…?

 

.columns

Index(['id', 'name', 'followers', 'popularity', 'genres', 'main_genre'], dtype='object')
Index(['id', 'name', 'album_name', 'artists', 'danceability', 'energy', 'key',
       'loudness', 'mode', 'speechiness', 'acousticness', 'instrumentalness',
       'liveness', 'valence', 'tempo', 'duration_ms', 'lyrics', 'year',
       'genre', 'popularity', 'total_artist_followers',
       'avg_artist_popularity', 'artist_ids', 'niche_genres'],
      dtype='object')

 

전처리

결측값 확인 

na_artist = artist_df.query('name.isna()').index
# 아이디로 조회가 안되는데 아이디가 왜 있는건지 모르겠음. 
for idx in na_artist:
    artist_df.loc[idx] = 'unknown'

artist_df.isna().sum()

아이디는 왜 있는건지 모르겠는게 저걸로 검색이 안됨.

 

na1_song = song_df.query('name.isna()').index
# 곡명이 언노운이면 뭐 나보고 어쩌라는겨... 

na2_song = song_df.query('album_name.isna()').index
# 앨범이 언노운이면 뭐 우째야되나... 

for idx in na1_song:
    song_df.loc[idx, 'name'] = 'untitled'

for idx in na2_song:
    song_df.loc[idx, 'album_name'] = 'various'

song_df.isna().sum()

얘도 걍 때웁니다.

 

범주화

# 팔로워 범주화 
# 1억 저건 뭐 브루노마스임? ㄷㄷ 
artist_df['followers'].min(), artist_df['followers'].max()

follower_label = np.linspace(0, 168508682, 11)
labels = [f"Group {i}" for i in range(1, 11)]

artist_df['follower_group'] = pd.cut(artist_df['followers'], bins = follower_label, labels = labels)

일단 아티스트는 넘파이의 힘을 빌려서 걍 째버림… 이거 리눅스로 하는 중인데 자동완성도 안되고 크롬이나 VS코드나 실시간으로 뻗는중임..

 

popularity_bins = list(range(0, 110, 10))
popularity_label = list(range(0, 100, 10))

# 이걸 넣어주면 0도 포함됩니다. 
artist_df['popularity_group'] = pd.cut(artist_df['popularity'], bins = popularity_bins, labels = popularity_label, include_lowest=True)

저 인클루드 어쩌고 안 주면 어떻게 되냐고요? 0이 Nan이 됩니다.

 

song_df['year'].min(), song_df['year'].max()

# 노래들 년도 볌주화
song_era = list(range(1900, 2040, 10)) # 1900년대부터 10틱으로 갑니다. 
song_era_label = [f"{i}s" for i in song_era[:-1]]

song_df['Era'] = pd.cut(song_df['year'], bins = song_era, labels = song_era_label, include_lowest = True, right = False)

노래... 뭐가 많은데 일단 발매년도랑...

 

popularity_bins = list(range(0, 110, 10))
popularity_label = list(range(0, 100, 10))

# 이걸 넣어주면 0도 포함됩니다. 
song_df['popularity_group'] = pd.cut(song_df['popularity'], bins = popularity_bins, labels = popularity_label, include_lowest=True)
follower_label = np.linspace(0, 295181876, 11)
labels = [f"Group {i}" for i in range(1, 11)]

song_df['follower_group'] = pd.cut(song_df['total_artist_followers'], bins = follower_label, labels = labels)

팔로워랑 인기도 있어서 이것도 범주화 해주고...

 

분석을 들어가긴 들어가야되는데 문제가 하나 있다. 지금 리눅스로 작성중인데 이게 숨쉬다가 VScode가 뻗고 크롬이 뻗고 파폭이 뻗고 그 와중에 티스토리는 자동저장이 안되고 브라우저 뻗으면 다 날아감. 그래서 여기까지 올리고 그 다음껀 낼 올려드림…

반응형

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

Post-COVID Video Games Worldwide (2021-2025)  (0) 2026.02.10
또 ChEMBL을 털어보았다  (0) 2026.01.28
캐글 EDA-마! 서퍼티파이! (2)  (0) 2026.01.20
그냥 해보는 ChEMBL EDA  (0) 2026.01.15
캐글 EDA-Video game sales  (0) 2026.01.09
Lv. 36 라이츄

Lv. 36 라이츄

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

그냥 해보는 ChEMBL EDA

Coding/EDA 2026. 1. 15. 19:40
반응형

이걸 근데 카테고리를 만들어야될지는 모르겠음… 이걸 매일 할 것 같지는 않고…

 

켐블서 특정 분자나 질환 치면 관련 화합물이 쭈루룩 나옵니다. 그거갖고 한거임.


거 정보좀 봅시다

df.shape()

(63, 29)

켐블 데이터 특: 칼럼 엄청 많음

 

df.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 63 entries, 0 to 62
Data columns (total 29 columns):
 #   Column              Non-Null Count  Dtype  
---  ------              --------------  -----  
 0   ChEMBL ID           63 non-null     object 
 1   Name                62 non-null     object 
 2   Synonyms            59 non-null     object 
 3   Type                63 non-null     object 
 4   Max Phase           59 non-null     float64
 5   Molecular Weight    63 non-null     float64
 6   Targets             40 non-null     float64
 7   Bioactivities       40 non-null     float64
 8   AlogP               63 non-null     float64
 9   Polar Surface Area  63 non-null     float64
 10  HBA                 63 non-null     int64  
 11  HBD                 63 non-null     int64  
 12  #RO5 Violations     63 non-null     int64  
 13  #Rotatable Bonds    63 non-null     int64  
 14  Passes Ro3          63 non-null     object 
 15  QED Weighted        63 non-null     float64
 16  Aromatic Rings      63 non-null     int64  
 17  Structure Type      63 non-null     object 
 18  Inorganic Flag      63 non-null     int64  
 19  Heavy Atoms         63 non-null     int64  
 20  Np Likeness Score   63 non-null     float64
 21  Molecular Formula   63 non-null     object 
 22  Smiles              63 non-null     object 
 23  Inchi Key           63 non-null     object 
 24  Inchi               63 non-null     object 
 25  Withdrawn Flag      63 non-null     bool   
 26  Orphan              63 non-null     int64  
 27  Records Key         63 non-null     object 
 28  Records Name        63 non-null     object 
dtypes: bool(1), float64(8), int64(8), object(12)
memory usage: 14.0+ KB

일단 여기서 미리 말하자면, NaN이 꽤 있는데 안채웠다. 왜요? 없어서 못 채웁니다. 임상(Max Phase)가 4.0이 아닌 약물들은 대부분 검색도 빡세고 켐블 최신 데이터까지 뒤져봤지만 역시나 NaN이었거든…

 

df.describe()

	Max Phase	Molecular Weight	Targets	Bioactivities	AlogP	Polar Surface Area	HBA	HBD	#RO5 Violations	#Rotatable Bonds	QED Weighted	Aromatic Rings	Inorganic Flag	Heavy Atoms	Np Likeness Score	Orphan
count	59.000000	63.000000	40.000000	40.00000	63.000000	63.000000	63.000000	63.000000	63.000000	63.000000	63.000000	63.000000	63.000000	63.000000	63.000000	63.000000
mean	2.474576	286.804603	61.400000	282.35000	3.535079	48.665238	2.317460	0.968254	0.063492	3.984127	0.803175	1.841270	-0.063492	19.571429	-0.363492	-0.063492
std	1.164947	64.184537	87.147914	691.58282	0.694833	11.659890	1.044577	0.567060	0.245805	1.539712	0.097299	0.676956	0.245805	3.509040	0.536603	0.245805
min	-1.000000	206.280000	1.000000	1.00000	2.100000	29.540000	1.000000	0.000000	0.000000	2.000000	0.490000	1.000000	-1.000000	15.000000	-1.510000	-1.000000
25%	2.000000	248.805000	2.000000	5.75000	3.070000	37.300000	1.000000	1.000000	0.000000	3.000000	0.755000	1.000000	0.000000	18.000000	-0.785000	0.000000
50%	2.000000	266.300000	8.500000	16.00000	3.430000	49.850000	2.000000	1.000000	0.000000	4.000000	0.820000	2.000000	0.000000	19.000000	-0.420000	0.000000
75%	4.000000	300.045000	102.750000	239.75000	3.825000	54.485000	3.000000	1.000000	0.000000	4.000000	0.865000	2.000000	0.000000	21.000000	0.070000	0.000000
max	4.000000	558.640000	375.000000	4089.00000	5.880000	86.630000	5.000000	3.000000	1.000000	9.000000	0.940000	3.000000	0.000000	33.000000	0.920000	0.000000

여기 있다고 다 수치형 아님. Max Phase도 엄밀히 말하자면 값만 숫자지 사실상 범주형이다.

 

	ChEMBL ID	Name	Synonyms	Type	Passes Ro3	Structure Type	Molecular Formula	Smiles	Inchi Key	Inchi	Records Key	Records Name
count	63	62	59	63	63	63	63	63	63	63	63	63
unique	63	62	59	1	2	1	53	63	63	63	63	63
top	CHEMBL253765	(R)-SUPROFEN	CLIPROFEN|CLIPROFENE|CLIPROFENO|R-25,160|R-25160	Small molecule	N	MOL	C14H12O3S	C[C@@H](C(=O)O)c1ccc(C(=O)c2cccs2)cc1	MDKGKXOCJGEUJW-SECBINFHSA-N	InChI=1S/C14H12O3S/c1-9(14(16)17)10-4-6-11(7-5...	['SID11112759', '212, suprofen (R)', 'Suprofen...	['SID11112759', '(R)-2-(4-(thiophene-5-carbony...
freq	1	1	1	63	61	63	3	1	1	1	1	1

값이 숫자가 아닌 애들은 이쪽.

 

df.isna().sum()

ChEMBL ID              0
Name                   1
Synonyms               4
Type                   0
Max Phase              4
Molecular Weight       0
Targets               23
Bioactivities         23
AlogP                  0
Polar Surface Area     0
HBA                    0
HBD                    0
#RO5 Violations        0
#Rotatable Bonds       0
Passes Ro3             0
QED Weighted           0
Aromatic Rings         0
Structure Type         0
Inorganic Flag         0
Heavy Atoms            0
Np Likeness Score      0
Molecular Formula      0
Smiles                 0
Inchi Key              0
Inchi                  0
Withdrawn Flag         0
Orphan                 0
Records Key            0
Records Name           0
dtype: int64

아 저 바이오액티비티스 결측값인 애들 확인해볼걸..

 

df.head()

ChEMBL ID	Name	Synonyms	Type	Max Phase	Molecular Weight	Targets	Bioactivities	AlogP	Polar Surface Area	...	Heavy Atoms	Np Likeness Score	Molecular Formula	Smiles	Inchi Key	Inchi	Withdrawn Flag	Orphan	Records Key	Records Name
0	CHEMBL253765	(R)-SUPROFEN	NaN	Small molecule	NaN	260.31	12.0	15.0	3.17	54.37	...	18	-0.81	C14H12O3S	C[C@@H](C(=O)O)c1ccc(C(=O)c2cccs2)cc1	MDKGKXOCJGEUJW-SECBINFHSA-N	InChI=1S/C14H12O3S/c1-9(14(16)17)10-4-6-11(7-5...	False	-1	['SID11112759', '212, suprofen (R)', 'Suprofen...	['SID11112759', '(R)-2-(4-(thiophene-5-carbony...
1	CHEMBL2104170	CLIPROFEN	CLIPROFEN|CLIPROFENE|CLIPROFENO|R-25,160|R-25160	Small molecule	2.0	294.76	NaN	NaN	3.82	54.37	...	19	-1.23	C14H11ClO3S	CC(C(=O)O)c1ccc(C(=O)c2cccs2)c(Cl)c1	NLGUJWNOGYWZBI-UHFFFAOYSA-N	InChI=1S/C14H11ClO3S/c1-8(14(17)18)9-4-5-10(11...	False	0	['CLIPROFEN', 'CLIPROFEN']	['CLIPROFEN', 'CLIPROFEN']
2	CHEMBL2107442	LOBUPROFEN	LOBUPROFEN|LOBUPROFENE|LOBUPROFENO	Small molecule	2.0	429.00	NaN	NaN	5.01	32.78	...	30	-1.12	C25H33ClN2O2	CC(C)Cc1ccc(C(C)C(=O)OCCN2CCN(c3cccc(Cl)c3)CC2...	JFGXBHHLHQAGRR-UHFFFAOYSA-N	InChI=1S/C25H33ClN2O2/c1-19(2)17-21-7-9-22(10-...	False	0	['LOBUPROFEN']	['LOBUPROFEN']
3	CHEMBL4297164	XIMOPROFEN	XIMOPROFEN|XIMOPROFENE|XIMOPROFENO	Small molecule	2.0	261.32	1.0	1.0	3.36	69.89	...	19	0.16	C15H19NO3	CC(C(=O)O)c1ccc(C2CCCC(=NO)C2)cc1	IQPPOXSMSDPZKU-UHFFFAOYSA-N	InChI=1S/C15H19NO3/c1-10(15(17)18)11-5-7-12(8-...	False	0	['Ximoprofen', 'XIMOPROFEN']	['Ximoprofen', 'XIMOPROFEN']
4	CHEMBL2107432	LOSMIPROFEN	LOSMIPROFEN|LOSMIPROFENE|LOSMIPROFENO	Small molecule	2.0	318.76	NaN	NaN	3.73	63.60	...	22	-0.76	C17H15ClO4	Cc1c(OC(C)C(=O)O)cccc1C(=O)c1ccc(Cl)cc1	JQYJAGNNCNBFRM-UHFFFAOYSA-N	InChI=1S/C17H15ClO4/c1-10-14(16(19)12-6-8-13(1...	False	0	['LOSMIPROFEN']	['LOSMIPROFEN']

이건 그냥 아 이렇게 있구나만 보십쇼.

 

df.column

Index(['ChEMBL ID', 'Name', 'Synonyms', 'Type', 'Max Phase',
       'Molecular Weight', 'Targets', 'Bioactivities', 'AlogP',
       'Polar Surface Area', 'HBA', 'HBD', '#RO5 Violations',
       '#Rotatable Bonds', 'Passes Ro3', 'QED Weighted', 'Aromatic Rings',
       'Structure Type', 'Inorganic Flag', 'Heavy Atoms', 'Np Likeness Score',
       'Molecular Formula', 'Smiles', 'Inchi Key', 'Inchi', 'Withdrawn Flag',
       'Orphan', 'Records Key', 'Records Name'],
      dtype='object')

보다보면 여기서 견적 나옴.

 

전처리

위에도 말했지만 결측값 이거는 우리가 못때워요... 몰라 안나와... 그래서 여기서는 범주화만 할 거다.

 

# 분자량이 500보다 크면 헤비 아니면 라이트
data_df['Moecular_classification'] = data_df['Molecular Weight'].apply(lambda x: 'Heavy' if x > 500 else 'Light')

아니 왜 하필 500이죠? 리핀스키의 5대 규칙에서 마지노선을 500으로 정했음. 저 람다 뭐죠? 아니 칼럼 둘로 나누는데 굳이 기명함수가 필요한가 해서 람다식 씀. 범주가 다 이래요.

 

# 4: Approved/3~0: Clinical/-1: Failed
data_df['Status'] = data_df['Max Phase'].map({4: 'Approved', 3: 'Clinical', 2: 'Clinical', 1: 'Clinical', 0: 'Clinical', -1: 'Failed'}) 
# 아니 맵합수가 왜 여기서

나는 백준 풀 때나 보던 맵이 나와서 당황했고… 이건 Max phase에 따라 범주화한건데, Max Phase가 4면 승인된(시장에 발은 들인)거고, 3~0이면 임상 단계인거고, -1이면 엎은거다.

 

본게임은 지금부터다

임상 단계별 분자량 평균

data_df.groupby('Status')['Molecular Weight'].mean() # 평균
Status
Approved    279.101176
Clinical    293.348500
Failed      272.860000
Name: Molecular Weight, dtype: float64

근데 우리가 얘만 봐서는 이게 분포가 어떤지 몰라요. 그니까 빡스플롯을 그려보자 이거다. 근데 이제 스웜플롯을 곁들인.

 

# boxplot
sns.boxplot(data_df, x = 'Status', y = 'Molecular Weight', hue = 'Status')
sns.swarmplot(data=data_df, x='Status', y='Molecular Weight', color='red', alpha=0.5)
plt.title('Molecular Weight by Status')
plt.show()

보니까 clinical에 있는 이상치들은 그래도 500은 안 넘었는데, 저기 500 넘은 놈이 하나 있네? 너 누구야!

 

범인은 FENOPROFEN CALCIUM이라는 놈인데, 얘는 그 자체로는 분자량이 그렇게 크지 않다. 근데 왜 500이 넘는데요? 쟤가 구조상 카복실산(COOH)이 달려있는데 카복실산이 음전하거든요? 그니까 단독으로 두면 안정하지 아니할 거 아니예요. 그래서 칼슘이랑 걔랑 뭔 상관이냐고? 칼슘이 2가 양이온이니까 저 페노프로펜이라는 친구를 양 옆구리에 하나씩 끼고 있는거다.

 

clinical에 있는 이상치들은 다 뭘 붙여서 몸집이 커진 케이스.

 

Bioactivities

약을 먹으면 우리 몸에 들어와서 일을 하겠죠? 그 활성 정도를 말한다.

 

data_df.groupby('Status')['Bioactivities'].mean() # 평균
Status
Approved    680.666667
Clinical     53.052632
Failed       24.500000
Name: Bioactivities, dtype: float64

이상하다? 쟤 평균이 왜 저렇게 높지? 결측값이 다 밑에 몰렸나? 그게 아님.

 

# boxplot
sns.boxplot(data_df, x = 'Status', y = 'Bioactivities', hue = 'Status')
sns.swarmplot(data=data_df, x='Status', y='Bioactivities', color='red', alpha=0.5)
plt.title('Bioactivities by Status')
plt.show()

저 봐요 저기 이상치 있는거. 저것때문에 평균 망한거임. 그래서 쟤는 뭔데 활성도가 저렇게 높아요? 이부프로펜 본인이요.

 

# 맨 휫흐니 검정을 할거예요 
# 얘네는 수가 적어서 t-test 못해요 
clinical_vals = data_df[data_df['Status'] == 'Clinical']['Bioactivities'].dropna() # 클리니컬
failed_vals = data_df[data_df['Status'] == 'Failed']['Bioactivities'].dropna() # 엎음

u_stat, p_val = stats.mannwhitneyu(clinical_vals, failed_vals, alternative='two-sided')

print(f"Mann-Whitney U statistic: {u_stat}")
print(f"P-value: {p_val}")

if p_val < 0.05:
    print("결과: 두 그룹 간에 통계적으로 유의미한 차이가 있습니다! (다른 놈임)")
else:
    print("결과: P-value가 0.05보다 큽니다. 두 그룹은 통계적으로 '그놈이 그놈'입니다.")

저 옆에놈들이 진짜로 또이또이인지 통계검정을 해 볼건데, 맨-휘트니 검정을 할 거다. 아니 t-test 어디감? t-test를 하려면 표본이 충분히 크거나 정규성을 만족해야되는데 Failed에 딸랑 두개있음. 그 크기 마지노선이 30임다.

 

Mann-Whitney U statistic: 12.0
P-value: 0.43372561703125134
결과: P-value가 0.05보다 큽니다. 두 그룹은 통계적으로 '그놈이 그놈'입니다.

그놈이 그놈이래.

 

AlogP

data_df.groupby('Status')['AlogP'].mean() # 평균
Status
Approved    3.369412
Clinical    3.675500
Failed      3.355000
Name: AlogP, dtype: float64

얘는 뭐냐면 이 분자가 얼마나 기름진가의 지표이다. 얘도 5 넘어가면 안된다.

 

# boxplot
sns.boxplot(data_df, x = 'Status', y = 'AlogP', hue = 'Status')
sns.swarmplot(data=data_df, x='Status', y='AlogP', color='red', alpha=0.5)
plt.title('AlogP by Status')
plt.show()

엎은 애들이랑 승인됨에는 없는데 임상 진행중인 애들중에 이상치가 꽤 보인다. 저 중에서 하나는 동물용 소염진통제, 하나는 항진균제(바르는거)이고 나머지 두 개는 정보가 없음.

 

Withdrawn Flag

모든 약에는 부작용이 있다. 이 플래그는 부작용 관련된건데... 아니 근데 다 부작용이 있는데 저 플래그는 뭐임? 그 부작용에도 '정도'가 있어요. 그 부작용이 아 이거 터지면 사람 죽는다 내지는 와 이거 심각한데 수준이면 저 플래그가 선다고 보면 된다. 대표적인 예시가 탈리도마이드.

 

- Benoxaprofen: 간 괴사, 광과민성, 담즙정체성 황달
- Suprofen: 신장 독성 (옆구리 통증 증후군)
- Pirprofen: 심각한 간독성
- Indoprofen: 위장관 출혈 및 발암성 의혹

 

이런 이유로 승인됐다가 시장에서 빠졌다고 보면 된다. 얘들도 나중에 다른 쓸모가 발견된다면 다시 쓰일지는 모르겠지만… 탈리도마이드도 다른 쓸모를 찾았음.

 

HBA, HBD

# FacetGrid를 사용해 Status별로 나눠서 보기
g = sns.FacetGrid(data_df, col="Status", height=4, aspect=1.2, col_order=['Approved', 'Clinical', 'Failed'])
g.map_dataframe(sns.histplot, x="HBA", y="HBD", discrete=(True, True), cbar=True, cmap="YlGnBu")

g.set_axis_labels("HBA (Acceptors)", "HBD (Donors)")
g.set_titles("{col_name} Group")
plt.tight_layout()
plt.show()

클리니컬은 뭔가 중구난방이구만. 참고로 둘다 5 넘어가면 안된다.

 

RO5 violation

data_df.groupby(['Status','#RO5 Violations']).size() # 분포 왜이래요
Status    #RO5 Violations
Approved  0                  17
Clinical  0                  36
          1                   4
Failed    0                   2
dtype: int64

이거 룰인데 어겨도 돼요? 라고 하실 수도 있는데 이걸 준수하면 좋은거지 뭔 법같은 게 아님. 당장 항체(주사형태)는 옛저녁에 RO5 위반했지만 잘 쓰고 있잖아요?

 

그래서 범인이 누구냐... 저기 AlogP 5 넘는 애들이요.

 

결론

1. 원조가 짱이다(이부프로펜 활성이 제일높음)

2. RO5를 위반하지 않더라도 부작용때문에 시장에서 퇴출될 수 있다. 승인되더라도 오래 살아남는 약은 없다. 

3. 탈리도마이드같은 게 또 있었네.

반응형

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

Post-COVID Video Games Worldwide (2021-2025)  (0) 2026.02.10
또 ChEMBL을 털어보았다  (0) 2026.01.28
캐글 EDA-마! 서퍼티파이! (2)  (0) 2026.01.20
캐글 EDA-마! 서퍼티파이!  (0) 2026.01.19
캐글 EDA-Video game sales  (0) 2026.01.09
Lv. 36 라이츄

Lv. 36 라이츄

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

캐글 EDA-Video game sales

Coding/EDA 2026. 1. 9. 23:40
반응형

https://www.kaggle.com/datasets/gregorut/videogamesales

 

Video Game Sales

Analyze sales data from more than 16,500 games.

www.kaggle.com

본인은 겜덕후다. 자타공인 겜덕후이고 모바일 피씨 콘솔 할 것 없이 어지간한 게임은 다 했으며 가끔 무지하게 주관적인 리뷰를 올리기도 하고 집에는 각종 노랑뚱띵이와 굿즈들이 가득한.

 

그런데 비디오 게임 판매량 데이터??? 야이씨 이건 겜덕후로써 못참지! 해서 제미나이를 부려먹었다.


모듈

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# 그래프 기본 테마 설정
sns.set_theme(palette="icefire", style="darkgrid", font_scale=1)
sns.color_palette("icefire", as_cmap=True)

# 그래프를 그리기 위한 기본 설정
plt.rcParams['font.family'] = 'NanumSquare'
# plt.rcParams['font.family'] = 'AppleGothic'
plt.rcParams['figure.figsize'] = 12, 9
plt.rcParams['font.size'] = 16
plt.rcParams['axes.unicode_minus'] = False

딱 필요한 것만 있음.


데이터 정보 확인

df.info()

video_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 16598 entries, 0 to 16597
Data columns (total 11 columns):
 #   Column        Non-Null Count  Dtype  
---  ------        --------------  -----  
 0   Rank          16598 non-null  int64  
 1   Name          16598 non-null  object 
 2   Platform      16598 non-null  object 
 3   Year          16327 non-null  float64
 4   Genre         16598 non-null  object 
 5   Publisher     16540 non-null  object 
 6   NA_Sales      16598 non-null  float64
 7   EU_Sales      16598 non-null  float64
 8   JP_Sales      16598 non-null  float64
 9   Other_Sales   16598 non-null  float64
 10  Global_Sales  16598 non-null  float64
dtypes: float64(6), int64(1), object(4)
memory usage: 1.4+ MB

저기 몇개 카운트 안 맞는 것 같다고? 결측값이 껴있으니까요.

 

df.describe()

video_df.describe()

여기 들어있다고 다 수치형인 건 아니고 범주형이 있을 수도 있다. 숫자면 다 저따구로 나와서... 내가 볼 때 등수랑 년도는 수치형 아님.

 

video_df.describe(include='O')

범주형 데이터는 이렇게 나온다. 근데 왜 밝은 배경이냐고? 지금 깃헙에서 찍고 있으니까요.

 

df.isna().sum()

video_df.isna().sum() # 아놔 결측값..
Rank              0
Name              0
Platform          0
Year            271
Genre             0
Publisher        58
NA_Sales          0
EU_Sales          0
JP_Sales          0
Other_Sales       0
Global_Sales      0
dtype: int64

왜 슬픈 예감은 틀리질 않는가…

 

사실 결측값을 채우는데는 여러가지 방법이 있지만 가장 비효율적이고 개같이 오래걸리는 겁나 브루트포스 뺨치는 방식이 바로 직접 찾아서 채우는거다. 갑자기 그 얘기가 왜 나와요? 빠진거 찾아서 일일이 채웠으니까. 일부 발매일이 지역별로 다른 게임은 북미 기준으로 채웠다.

 

df.head()

video_df.head()

이건 걍 위에 다섯개 뭔지 보는거임.

 

df.columns

video_df.columns
Index(['Rank', 'Name', 'Platform', 'Year', 'Genre', 'Publisher', 'NA_Sales',
       'EU_Sales', 'JP_Sales', 'Other_Sales', 'Global_Sales'],
      dtype='object')

음 칼럼이 이렇게 있군.

 

범주 만들기

아 결측값이요? 그거 못올려 짤려. 쓸데없이 스크롤 길다고 백퍼 님들 짜증내요.

 

플랫폼 제조사별 범주

video_df['Platform'].unique() # 플랫폼 목록

롸? 데이터에 뭐가 어떻게 들어있길래 그래요? 저기에는 우리가 살면서 봐 왔던(혹은 사 왔던, 혹은 거쳐 간) 모든 게임기 기종이 들어있다. 엑박은 세종류인데 닌텐도만 해도 게임보이 GBA(게임보이 어드밴스) DS 3DS 위… 그래서 이걸 제조사별로 묶는 칼럼을 하나 만드는거다. 예를 들어서 플스는 소니 이런 식으로. 그래야 플엑닌 보지.

 

platform_map = {
    # Nintendo
    'Wii': 'Nintendo', 'NES': 'Nintendo', 'GB': 'Nintendo', 'DS': 'Nintendo', 
    'SNES': 'Nintendo', 'GBA': 'Nintendo', '3DS': 'Nintendo', 'N64': 'Nintendo', 
    'GC': 'Nintendo', 'WiiU': 'Nintendo',
    
    # Sony
    'PS3': 'Sony', 'PS2': 'Sony', 'PS4': 'Sony', 'PS': 'Sony', 
    'PSP': 'Sony', 'PSV': 'Sony',
    
    # Microsoft
    'X360': 'Microsoft', 'XB': 'Microsoft', 'XOne': 'Microsoft',
    
    # SEGA
    'GEN': 'Sega', 'DC': 'Sega', 'SAT': 'Sega', 'SCD': 'Sega', 'GG': 'Sega',
    
    # PC & Others
    'PC': 'PC', '2600': 'Atari', 'WS': 'Bandai', 'NG': 'SNK', 
    'TG16': 'NEC', '3DO': '3DO', 'PCFX': 'NEC'
}

# 'Platform_Vendor'라는 새로운 칼럼 만들기
video_df['Platform_Vendor'] = video_df['Platform'].map(platform_map)

나도 모르는 기기들이 많음. 발매됐을때 엄마반 아빠반이었던 것도 있음. (사유: 아직 감수분열도 안해서)

 

퍼블리셔 범주화

# {기존 퍼블리셔 이름: (계열사 그룹, 국적)}
pub_info_map = {
    # 일본계 (Japan)
    'Nintendo': ('Nintendo', 'Japan'),
    'Sony Computer Entertainment': ('Sony', 'Japan'),
    'Sony Computer Entertainment Europe': ('Sony', 'Japan'),
    'Sony Computer Entertainment America': ('Sony', 'Japan'),
    'Sega': ('Sega Sammy', 'Japan'),
    'Sammy Corporation': ('Sega Sammy', 'Japan'),
    'Capcom': ('Capcom', 'Japan'),
    'Konami Digital Entertainment': ('Konami', 'Japan'),
    'Namco Bandai Games': ('Bandai Namco', 'Japan'),
    'Banpresto': ('Bandai Namco', 'Japan'),
    'Square Enix': ('Square Enix', 'Japan'),
    'SquareSoft': ('Square Enix', 'Japan'),
    'Enix Corporation': ('Square Enix', 'Japan'),
    'Tecmo Koei': ('Koei Tecmo', 'Japan'),
    'Atlus': ('Sega Sammy', 'Japan'),
    
    # 북미계 (North America)
    'Activision': ('Activision Blizzard', 'North America'),
    'Activision Blizzard': ('Activision Blizzard', 'North America'),
    'Electronic Arts': ('EA', 'North America'),
    'EA Sports': ('EA', 'North America'),
    'Take-Two Interactive': ('Take-Two', 'North America'),
    'Rockstar Games': ('Take-Two', 'North America'), # 목록에 있을 경우
    'Microsoft Game Studios': ('Microsoft', 'North America'),
    'Bethesda Softworks': ('ZeniMax', 'North America'),
    'Warner Bros. Interactive Entertainment': ('Warner Bros', 'North America'),
    'THQ': ('THQ', 'North America'),
    
    # 유럽계 (Europe)
    'Ubisoft': ('Ubisoft', 'Europe'),
    'Ubisoft Annecy': ('Ubisoft', 'Europe'),
    'Codemasters': ('Codemasters', 'Europe'),
    '505 Games': ('Digital Bros', 'Europe'),
    'Deep Silver': ('Koch Media', 'Europe'),
    'Koch Media': ('Koch Media', 'Europe')
}

# 데이터프레임에 적용
video_df['Group'] = video_df['Publisher'].map(lambda x: pub_info_map.get(x, ('Others', 'Unknown'))[0])
video_df['Country'] = video_df['Publisher'].map(lambda x: pub_info_map.get(x, ('Others', 'Unknown'))[1])

별 건 아니고 게임 퍼블리셔를 제조사/국적으로 묶는거다. 저기 보면 코나미, 반다이 남코, 스퀘어 에닉스 보여요? 다 일본 회사잖음. 그거다. ㅇㅋ?

 

년도 그루핑

bins = [-1, 0, 1980, 1990, 2000, 2010, 2020, 2025]
labels = ['Unknown', 'Pre-1980', '1980s', '1990s', '2000s', '2010s', '2020s']

video_df['Era'] = pd.cut(video_df['Year'], bins=bins, labels=labels)

아니 뭐 이딴것까지 묶어요? 라고 생각하실 수도 있는데, 1990년대에 발매된 게임들을 볼 때 1990년부터 1999년까지 찾는 개노가다를 분석하는 코드에서 하고 싶지 않으면 할 수 있을 때 묶으십쇼.

 

플.엑.닌 삼국지

플.엑.닌은 플레이스테이션(공기청정기 아님), 엑스박스, 닌텐도다. 겜덕후들이라면 적어도 저 셋 중 하나 이상은 집에 있거나 구매 의사가 있을수도 있고, 겜덕후가 아니더라도 한번쯤은 들어봤으며, 특히 애 있는 집안에서 애들 시켜주면 껌뻑 죽는 그거 맞다. 얘네들이 하루아침에 응애 나 닌텐도! 이러고 나온 게 아니라 각각 태동기, 전성기, 흑역사(...)가 있고 특징이 있단말이죠. 그니까 함 보자.

 

얼마나 팔았나

# 전체 플랫폼(회사)별로 몇 개나 있나요?
video_df.groupby('Platform_Vendor')[['Rank']].count()

닌텐도 얘네는 뭐 게임 찍어내나요? 그게 아니라 닌텐도가 역사가 조낸 길어요... 나중에 얘기해드림.

 

# 플. 엑. 닌은 각각 얼마나 팔았을까? 
console_three = ['Nintendo','Sony','Microsoft'] # 엑박이 마소꺼예요.. 
video_df.query('Platform_Vendor in @console_three').groupby('Platform_Vendor')[['Rank']].count()

아니 그럼 마소는 왜이렇게 없어요? 빌형 뭐함? 마소(엑박)는 닌텐도나 소니(플스)에 비해 늦게 나온 애들이라 그래요.

 

# 년도별로는 얼마나 팔았음?
console_three = ['Nintendo','Sony','Microsoft']
video_df.query('Platform_Vendor in @console_three').groupby(['Platform_Vendor', 'Era'], observed=False)[['Global_Sales']].sum().sort_values(by=['Platform_Vendor', 'Global_Sales'], ascending=False)

년도별 판매량을 보면 닌텐도와 소니 둘 다 1980년부터 찍히지만 마소는 1990년대부터 찍히죠? 엑박이 플스, 닌텐도에 비해 후발주자다. 80년대 데이터를 비교해보면 소니보다 닌텐도가 더 많은데, 플.엑.닌중에는 닌텐도가 제일 먼저 나왔다.

 

# 다른 나라에서도 그럴까요?
# NA: 북미, EU: 유럽, JA: 일본, Other: 기타 
console_three = ['Nintendo','Sony','Microsoft']
video_df.query('Platform_Vendor in @console_three').groupby(['Platform_Vendor', 'Era'], observed=False)[['NA_Sales','EU_Sales','JP_Sales','Other_Sales','Global_Sales']].sum().sort_values(by=['Platform_Vendor','Era'], ascending=False)

칼럼은 왼쪽부터 북미, 유럽, 일본, 기타(한국도 포함인듯), 글로벌이다. 아니 닌텐도 북미에서 생각보다 많이 해먹었네?

 

import matplotlib.pyplot as plt
import seaborn as sns

# 1. 그래프용 데이터 준비 (Global 제외)
plot_df = df_2000s_region[['NA_Sales', 'EU_Sales', 'JP_Sales', 'Other_Sales']]

# 2. 그래프 그리기
# 'stacked=False'가 핵심입니다 (기본값이 False라 생략 가능)
ax = plot_df.plot(kind='bar', 
                    figsize=(12, 7), 
                    width=0.8,      # 막대 두께 조절
                    rot=0)          # x축 이름(Nintendo 등) 똑바로

# 3. 디테일 설정
plt.title('2000s Regional Sales Comparison: Sony vs Nintendo vs Microsoft', fontsize=16, pad=20)
plt.ylabel('Sales (Millions)', fontsize=12)
plt.xlabel('Platform Vendor', fontsize=12)
plt.legend(title='Region', fontsize=10)
plt.grid(axis='y', linestyle='--', alpha=0.5)

# 막대 위에 수치 표시 (가독성 업그레이드)
for p in ax.patches:
    ax.annotate(f'{p.get_height():.1f}', 
                (p.get_x() + p.get_width() / 2., p.get_height()), 
                ha = 'center', va = 'center', 
                xytext = (0, 7), 
                textcoords = 'offset points',
                fontsize=9)

plt.tight_layout()
plt.show()

소니는 유럽, 기타 지역에서 닌텐도를 앞섰고 엑박은 일본에서 맥을 못 추죠? 이건 마소가 단순히 늦게 시작해서가 아니라, 얘네들이 엑박을 내놓긴 했는데 일단 컨트롤러가 서양인들 손 위주라서 동양인들에게는 너무 컸음. 그 저기 서양 형님들 떡대 봐봐요 장난아님.

 

장르별 실적+주력 장르

# 글로벌 판매량 기준으로 장르별 실적 비교
genre_pivot = (video_df.query('Platform_Vendor in @console_three')
                .groupby(['Platform_Vendor', 'Genre'], observed=False)['Global_Sales']
                .sum()
                .unstack())

# 가독성을 위해 배경색 입히기 (주피터 노트북 환경)
genre_pivot.style.format("{:.3f}").background_gradient(cmap='coolwarm', axis=1)

마소는 슈터하고 액션, 닌텐도는 플랫폼과 액션, Misc(뭐임?), 스포츠, 롤플레잉, 소니는 액션이랑 스포츠. 이 장르들에서 많이 해먹었다.

 

실제로도 보면 세 콘솔에서 유명한 게임이 다 다른데, 닌텐도는 마리오+젤다+동숲+스플래툰+커비를 비롯한 퍼스트 파티와 포켓몬(퍼스트 파티는 아님) 등, 다양한 IP를 활용해서 다양한 게임을 내는데 이 게임들을 닌텐도 기기를 사야만 할 수 있다. 그러니까 이거 할 거면 우리 기기를 반드시 사라! 이거임. 그리고 다양한 IP를 활용해서 다양한 게임들을 내기때문에 주력이라고 할 수 있을 장르도 여러개다.

 

플스는 내가 안해봐서(플스 잡아본 적 없음) 구글링했는데, 플스의 주력 게임 중 하나가 파이널 판타지이다. 티파! 에어리스! 그거 맞음. 달을 보고 있었다가 파판 16 OST인가 그래요. 그 켄시가 부른거 함 들어보십쇼. 그 외에도 갓 오브 워나 마블 스파이더맨, 호라이즌 제로 던도 있고 동아리에서 선배들이 많이 했던 위닝도 있었다.

 

엑박은 가장 많이 떠올리시는게 헤일로, 기어스 오브 워인데 둘다 일단 총겜이고... 데드스페이스도 엑박에서 나오지 않았나? 그리고 포르자 시리즈도 있다. 포르자는 총겜은 아님.

 

시대, 그리고 게임

# 이제 그룹바이 기준이 연도로 바뀌게 된다. 단, 연도가 0으로 된 것은 빼고. (발매 취소된 겜이라... )
video_df_cp = video_df.copy()
video_df_cp.drop(video_df_cp[video_df_cp['Year'] == 0].index, inplace=True)
video_df_cp['Era'] = video_df_cp['Era'].cat.remove_unused_categories()

video_df_cp.groupby(['Era', 'Platform'], observed=False)['Name'].count().reset_index(name='Game_Count')

저 데이터 안에 연도가 NaN인 게임 중 하나가 발매 취소된 게임이다. 그래서 퍼블리셔는 있는데 발매년도가 없다.

 

platform_pivot = video_df_cp.pivot_table(index='Era', columns='Platform', values='Global_Sales', aggfunc='sum', observed=False)
platform_pivot

피벗테이블이 가로로 너무 길어서 이건 캡쳐 못했음. 그럼 닌텐도가 왜 북미에서 짱짱맨이 되었는지를 얘기해보자.

 

아타리 쇼크를 아십니까? 들어본 사람도 있을거고, 그 시대에 살았던 사람도 계실거고, 처음 듣는 사람도 있을텐데 닌텐도 이전에 아타리가 있었다. 근데 년도를 잘 보니까 80년대에 나왔다가 90년대부터 판매량이 0이네요? 아타리 쇼크가 83년부터 85년까지였거든…

 

아타리가 플랫폼을 만드는 회사라는 건 알겠어. 근데 아타리 쇼크는 뭐고, 닌텐도가 어떻게 그 자리를 차지한거지? 에 대한 얘기를 하려면 아타리 쇼크 얘기를 해야 한다. 엔씨 아시죠 엔씨? 그 리니지 만드는 회사. 걔네가 처음부터 그렇게 욕먹던 회사가 아니었고, 블소 초창기에는 진짜 갓겜 만드는 회사였어요. 근데 지금은 게임 낸다고 쇼케이스 나오면 다들 반응이 그래서 과금 모델은 어떤데? 어차피 리니지라이크 아냐? 이러잖아. 나도 엔씨게임은 진짜 안하지만 갓겜시절에는 블소 즐기고 그랬거든요.

 

그래서 송재경님을 만난다면 하나 꼭 물어보고 싶은 게 '개고기탕후루'라는 말을 듣고 어떤 심정이었는지임. 개고기탕후루가 멸칭입니다, 멸칭. 아마 택진이형은 최초로 개고기탕후루라는 말을 만들어낸 사람을 진짜 애타게 찾고싶을거임.

 

갑자기 엔씨 얘기가 왜 나옴? 얘네가 리니지 블소 다 갓겜이었는데 개고기탕후루 소리를 왜 들어요? 그죠. 게임이 그놈이 그놈인데 리니지라이크에 과금은 무슨 차 한대값을 박아야 게임 속에서 숨을 쉴 수 있잖아요. 아타리도 비슷한 이유로 망했다고 보시면 됩니다. 엔씨는 리니지라이크가 돈이 되니까 그것만 찍어냈다가 개고기탕후루 소리를 듣는거고, 아타리는 게임이 돈이 된다고 서드파티들이 겁나 찍어냈는데 그게 지금 엔씨게임이랑 비교하는것조차 실례일 정도로 진짜 쌉똥겜 투성이라 그렇게 된 거다. 엔씨 영업이익 떨어졌다고 가끔 기사 뜨죠? 아타리 쇼크때 아타리는 그냥 조낸 망했습니다.

 

이 때 미국 게임 시장에 처음으로 뛰어든 선구자가 닌텐도였다. 근데 그때 사람들이 하도 개똥겜들을 많이 봐서 게임을 무슨 악마의 똥같은 걸로 취급하는 걸 본 닌텐도가 저희는 게임기가 아니라 엔터테인먼트 시스템(Entertainment System)입니다~ 하고 들어가서 북미 시장을 먹은거다.

 

그리고 그 '엔터테인먼트 시스템'이 NES다.

 

그 뒤에 소니가 쓱 참전해서 플.엑 구도가 된거고

 

빌형은 왜 거기 낀거임 근데?

 

의 압축버전. 

 

플랫폼별 일짱은 누구?

벤더별 일짱

# 1. 각 플랫폼별 Global_Sales가 가장 높은 행의 인덱스 추출
idx = video_df.groupby('Platform')['Global_Sales'].idxmax()

# 2. 해당 인덱스의 데이터만 추출하여 정리
top_titles = video_df.loc[idx, ['Platform_Vendor', 'Platform', 'Name', 'NA_Sales', 'EU_Sales', 'JP_Sales', 'Other_Sales', 'Global_Sales']]

# 3. 제조사(Vendor)와 플랫폼별로 보기 좋게 정렬
top_titles = top_titles.sort_values(['Platform_Vendor', 'Global_Sales'], ascending=[True, False])

# 소수점 3자리 설정 및 출력
top_titles.style.format({
    'NA_Sales': '{:.3f}', 'EU_Sales': '{:.3f}', 
    'JP_Sales': '{:.3f}', 'Other_Sales': '{:.3f}', 'Global_Sales': '{:.3f}'
}).background_gradient(cmap='coolwarm', subset=['Global_Sales'])

우리집에 위는 없었지만 동방에는 있었다. 근데 이게 신박하긴 했음. 지금도 스위치 조이콘 빼서 휘두른다거나 그런거 있죠? 점프로프 챌린지도 그렇고. 그걸 나는 동아리방에서 위 리모콘으로 마리오카트랑 복싱 하면서 처음 써봤음. 근데 그런것치고 압도적인데…?

 

포켓몬 XY같은 경우 처음으로 3DS로 나온거고 모든 포켓몬이 3D화 됐고, 피카츄의 울음소리가 성우 울음소리로 바꼈는데 왜 롤백했냐 아오... 그래서 1세대~5세대 포켓몬은 버전별 이미지 찾아보면 도트에서 3D 됐잖아요. 라이츄 똥배때문에 도트뚱쥐됐는데 3D되면서 살 빠진거임. 동생놈이 염무왕의 존재를 알았다면 내 별명은 염무왕이었겠지... 아무튼. 그리고 포켓몬 XY, ORAS, SM, USUM에 있는 시스템 중에 포켓몬 아미에(7세대는 리플레)라는 게 있다. 내 포켓몬을 쮸담쮸담하고 하이파이브고 하고 뭐 그런거 있음. 그것때문에 잘 팔린게 아닐까 추정중이다.

 

플랫폼별 일짱

# 1. 각 플랫폼별 매출 1위 게임 추출
top_idx = video_df_cp.groupby('Platform')['Global_Sales'].idxmax()
platform_tops = video_df_cp.loc[top_idx, ['Platform', 'Name', 'NA_Sales', 'EU_Sales', 'JP_Sales', 'Other_Sales', 'Global_Sales']]

# 2. 보기 편하게 판매량 순으로 정렬 (상위 15개 기종)
platform_tops = platform_tops.sort_values('Global_Sales', ascending=False).head(15)

# 3. 소수점 3자리 조절 및 스타일링
platform_tops.style.format("{:.3f}", subset=['NA_Sales', 'EU_Sales', 'JP_Sales', 'Other_Sales', 'Global_Sales'])\
                    .background_gradient(cmap='coolwarm', subset=['Global_Sales'])\
                    .bar(subset=['NA_Sales', 'EU_Sales', 'JP_Sales'], color='#FFA07A')\
                    .bar(subset=['Other_Sales'], color='#FFD700')

포켓몬이 3위야 세상에...

 

# 시각화를 위해 게임 이름을 인덱스로 설정
plot_data = platform_tops.set_index('Name')[['NA_Sales', 'EU_Sales', 'JP_Sales', 'Other_Sales']]

# 누적 막대 그래프 그리기
plot_data.plot(kind='barh', stacked=True, figsize=(12, 8), colormap='coolwarm')
plt.title('Sales Breakdown by Region for Top Titles per Platform', fontsize=15)
plt.xlabel('Sales (Millions)')
plt.ylabel('Top Game Title')
plt.legend(title='Region', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.show()

저는 wii를 안해봐서 쟤가 왜 잘 팔렸는지 모르겠습니다.

 

플랫폼, 그리고 시대별 일짱은?

# 1. Vendor와 Era별로 Global_Sales가 최대인 행의 인덱스를 추출
# observed=True를 써서 데이터가 없는 Unknown 시대는 제외합니다.
top_idx_era = video_df_cp.groupby(['Platform_Vendor', 'Era'], observed=True)['Global_Sales'].idxmax()

# 2. 해당 인덱스로 데이터프레임 생성
era_champions = video_df_cp.loc[top_idx_era, 
                                ['Platform_Vendor', 'Era', 'Platform', 'Name', 'NA_Sales', 'JP_Sales', 'Global_Sales']]

# 3. 가독성을 위해 정렬
era_champions = era_champions.sort_values(['Platform_Vendor', 'Era'])

# 4. 소수점 3자리와 스타일 적용
era_champions.style.format("{:.3f}", subset=['NA_Sales', 'JP_Sales', 'Global_Sales'])\
                    .background_gradient(cmap='coolwarm', subset=['Global_Sales'])

본인 참고로 키넥트도 안해봄. 집에 일단 콘솔 둘 데가 없습니다. ...팩맨이 혹시 우리가 아는 그 팩맨임?

 

에이, 원툴 아냐? 

# 1. 진영별 전체 매출 합계
vendor_total = video_df_cp.groupby('Platform_Vendor')['Global_Sales'].sum()

# 2. 진영별 상위 10개 게임의 매출 합계
top10_sum = (video_df_cp.sort_values('Global_Sales', ascending=False)
                .groupby('Platform_Vendor')
                .head(10)
                .groupby('Platform_Vendor')['Global_Sales'].sum())

# 3. 비중 계산 (단위: %)
dependency_df = pd.DataFrame({
    'Total_Sales': vendor_total,
    'Top10_Sales': top10_sum,
    'Dependency_Rate(%)': (top10_sum / vendor_total) * 100
}).sort_values('Dependency_Rate(%)', ascending=False)

dependency_df.style.format("{:.3f}")

플랫폼별 일짱이 전체 매출에 얼마나 기여했는지를 보자. 위스포츠가 잘나가긴 했지만, 그럼에도 10%정도다. 마소의 헤일로2나 플스의 다른 게임들도 마찬가지.

 

한짤요약

# 시각화를 위한 통합 대시보드 설정
fig, axes = plt.subplots(2, 2, figsize=(20, 15))
fig.suptitle('비디오 게임 콘솔 삼국지: 데이터로 본 반전의 역사', fontsize=25, fontweight='bold')

# 1. [좌상단] 시대별 플랫폼 제조사 매출 추이 (The History)
era_revenue = video_df_cp.groupby(['Era', 'Platform_Vendor'], observed=True)['Global_Sales'].sum().unstack().fillna(0)
era_revenue.plot(kind='area', stacked=True, ax=axes[0, 0], alpha=0.7, cmap='coolwarm')
axes[0, 0].set_title('① 시대별 진영 점유율: 아타리의 몰락과 닌텐도/소니의 부상', fontsize=15)
axes[0, 0].set_ylabel('Global Sales (Millions)')

# 2. [우상단] TOP 10 의존도 비교 (The Myth Buster)
sns.barplot(x=dependency_df.index, y='Dependency_Rate(%)', data=dependency_df, hue = dependency_df.index, ax=axes[0, 1], palette='coolwarm')
axes[0, 1].set_title('② "닌텐도는 원툴?" NO! 상위 10개 타이틀 매출 비중 비교', fontsize=15)
axes[0, 1].set_ylim(0, 110) # 3DO 등 소규모 벤더 제외하고 주요 3사 강조를 위해 조정 가능
for p in axes[0, 1].patches:
    axes[0, 1].annotate(f'{p.get_height():.1f}%', (p.get_x() + p.get_width() / 2., p.get_height()), 
                        ha='center', va='center', xytext=(0, 9), textcoords='offset points')

# 3. [좌하단] 주요 진영별 국가별 판매 비중 (Region Strategy)
vendor_region = video_df_cp.groupby('Platform_Vendor')[['NA_Sales', 'EU_Sales', 'JP_Sales']].sum()
vendor_region_pct = vendor_region.div(vendor_region.sum(axis=1), axis=0) * 100
vendor_region_pct.loc[['Nintendo', 'Sony', 'Microsoft']].plot(kind='barh', stacked=True, ax=axes[1, 0], colormap='coolwarm')
axes[1, 0].set_title('③ 진영별 국가별 공략지: 북미의 MS vs 일본의 닌텐도 vs 균형의 소니', fontsize=15)
axes[1, 0].set_xlabel('Sales Percentage (%)')

# 4. [우하단] 플랫폼별 일짱 게임의 위엄 (The Champions)
sns.barplot(x='Global_Sales', y='Name', data=platform_tops.head(10), hue='Global_Sales', ax=axes[1, 1], palette='coolwarm')
axes[1, 1].set_title('④ 역대 가장 강력했던 "일짱" 타이틀 TOP 10', fontsize=15)
axes[1, 1].set_xlabel('Global Sales (Millions)')

plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()

1. 태초에 아타리가 있었지만 아타리 쇼크로 개같이 멸망했고, 그 자리를 닌텐도가 '엔터테인먼트 시스템'을 표방하면서 들어왔다. 그 다음에 소니가 들어와서 플.엑 양대산맥이 됐다가 삘받았는지  빌횽이 엑박을 내면서 플.엑.닌 삼국지가 된 것.

2. 위 스포츠의 판매량이 압도적인 건 인정. 하지만 닌텐도가 그걸로 원툴이었던 건 아니다.

3. 플.엑.닌은 각자의 특징을 살려서 삼국지 시장전을 하고 있다.

4. 닌텐도는 자사의 퍼스트파티들(마리오, 젤다, 동숲, 커비, 스플래툰)과 세컨트파티(포켓몬 등) 등의 IP를 내세운 독점작을 통한 이거 하려면 우리거 사세요~ 전략과 마리오 파티, 마리오 카트처럼 아이들도 즐길 수 있는 '엔터테인먼트'를 표방한 게임들을 만들었다.

5. 소니는 닌텐도보다 한 발 늦었지만, 어른이들을 위한 세련된 콘솔과 다양한 써드파티 게임들로 고객들을 끌어들였다.

6. 마소는 진입 시기가 제일 늦었던데다가 초기에 있었던 여러가지 이슈들로 인해 데이터상으로 플.엑.닌중에서 제일 판매량이 적고 일본 매출도 저조하지만, 포르자 시리즈와 헤일로, 기어스 오브 워 등의 총게임으로 북미 시장을 책임지고 있다.

반응형

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

Post-COVID Video Games Worldwide (2021-2025)  (0) 2026.02.10
또 ChEMBL을 털어보았다  (0) 2026.01.28
캐글 EDA-마! 서퍼티파이! (2)  (0) 2026.01.20
캐글 EDA-마! 서퍼티파이!  (0) 2026.01.19
그냥 해보는 ChEMBL EDA  (0) 2026.01.15
Lv. 36 라이츄

Lv. 36 라이츄

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

데이터프레임의 정보를 확인하는 몇 가지 방법

Coding/Python 2026. 1. 8. 20:53
반응형

여러분은 데이터를 분석할 때 제일 먼저 해야 하는 게 뭔지 아십니까? 물론 데이터를 얻는걸 제일 먼저 해야겠지만, 데이터를 다 얻고 나면 데이터를 체크하고 전처리로 뭘 할 지를 정해야 한다. 근데 지금 리눅스 업글중이라 둘 다 다룰 수 있을지는 모르겠음.


df.info()

데이터프레임의 정보를 확인할 수 있다. 

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 299 entries, 0 to 298
Data columns (total 13 columns):
 #   Column                    Non-Null Count  Dtype  
---  ------                    --------------  -----  
 0   age                       299 non-null    float64
 1   anaemia                   299 non-null    int64  
 2   creatinine_phosphokinase  299 non-null    int64  
 3   diabetes                  299 non-null    int64  
 4   ejection_fraction         299 non-null    int64  
 5   high_blood_pressure       299 non-null    int64  
 6   platelets                 299 non-null    float64
 7   serum_creatinine          299 non-null    float64
 8   serum_sodium              299 non-null    int64  
 9   sex                       299 non-null    int64  
 10  smoking                   299 non-null    int64  
 11  time                      299 non-null    int64  
 12  DEATH_EVENT               299 non-null    int64  
dtypes: float64(3), int64(10)
memory usage: 30.5 KB

1. RangeIndex: 데이터의 인덱스(숫자 0~298까지)

2. #, column: 칼럼 번호랑 칼럼명

3. Non-Null Count: 결측값 없는 데이터 수. 이게 저 위에 RangeIndex랑 수가 딱 맞으면 결측값이 없는거다. 널 있으면 널 있는만큼 빠진다.

4. Dtype: 칼럼의 데이터 타입. 만약 문자가 쓰여진 칸이 있다면 object로 나온다.

 

df.describe()

1. 수치형 데이터

이거 썼을 때 수치형 데이터랑 범주형 데이터랑 나오는 결과가 다르다. 일단 걍 describe는 수치형 데이터에 대한 요약을 해 준다.

 

age	anaemia	creatinine_phosphokinase	diabetes	ejection_fraction	high_blood_pressure	platelets	serum_creatinine	serum_sodium	sex	smoking	time	DEATH_EVENT
count	299.000000	299.000000	299.000000	299.000000	299.000000	299.000000	299.000000	299.00000	299.000000	299.000000	299.00000	299.000000	299.00000
mean	60.833893	0.431438	581.839465	0.418060	38.083612	0.351171	263358.029264	1.39388	136.625418	0.648829	0.32107	130.260870	0.32107
std	11.894809	0.496107	970.287881	0.494067	11.834841	0.478136	97804.236869	1.03451	4.412477	0.478136	0.46767	77.614208	0.46767
min	40.000000	0.000000	23.000000	0.000000	14.000000	0.000000	25100.000000	0.50000	113.000000	0.000000	0.00000	4.000000	0.00000
25%	51.000000	0.000000	116.500000	0.000000	30.000000	0.000000	212500.000000	0.90000	134.000000	0.000000	0.00000	73.000000	0.00000
50%	60.000000	0.000000	250.000000	0.000000	38.000000	0.000000	262000.000000	1.10000	137.000000	1.000000	0.00000	115.000000	0.00000
75%	70.000000	1.000000	582.000000	1.000000	45.000000	1.000000	303500.000000	1.40000	140.000000	1.000000	1.00000	203.000000	1.00000
max	95.000000	1.000000	7861.000000	1.000000	80.000000	1.000000	850000.000000	9.40000	148.000000	1.000000	1.00000	285.000000	1.00000

수치형 데이터에 대해 describe를 쓰면 전체 데이터 개수, 평균, 표준편차, 최댓값, 최솟값, 사분위수(제 1사분위수, 제 2사분위수, 제 3사분위수)가 나온다. 근데 저게 수치형만 나오는 게 아니라 범주형이어도 값이 숫자면 다 나옴.

 

2. 범주형 데이터

얘는 R 서머리처럼 알아서 안 해주기때문에 범주형 데이터에 대한 요약을 보려면 include='O' 옵션을 줘야 한다.

 	Name 	Platform 	Genre 	Publisher
count 	16598 	16598 	16598 	16540
unique 	11493 	31 	12 	578
top 	Need for Speed: Most Wanted 	DS 	Action 	Electronic Arts
freq 	12 	2163 	3316 	1351

1. count: 값이 몇 개인가

2. unique: 고윳값이 몇 개인가(그니까 안 겹치는 값이요)

3. top: 최빈값

4. freq: 빈도(아마도 최빈값 빈도?)

 

df.isna().sum()

age                         0
anaemia                     0
creatinine_phosphokinase    0
diabetes                    0
ejection_fraction           0
high_blood_pressure         0
platelets                   0
serum_creatinine            0
serum_sodium                0
sex                         0
smoking                     0
time                        0
DEATH_EVENT                 0
dtype: int64

(결측값이 없는 데이터)

 

Rank              0
Name              0
Platform          0
Year            271
Genre             0
Publisher        58
NA_Sales          0
EU_Sales          0
JP_Sales          0
Other_Sales       0
Global_Sales      0
dtype: int64

(결측값이 있는 데이터)

 

.isna()를 쓰면 NaN인지 여부를 확인해주는거고 .isna().sum() 하면 칼럼에 있는 결측값이 몇 개인지 알려준다. 올 0이라고요? 축하합니다. 0이 아니라고요? 결측값 처리하셔야 합니다.

 

일괄로 채울거면 df.fillna(value) 쓰셔도 된다. 빼버릴거면 df.dropna()도 있다.

 

df.head()

해당 데이터프레임의 맨 위 행 5개를 보여준다. 괄호 안에 숫자를 쓰면 그 숫자만큼 보여주고, df.tail()을 쓰면 맨 뒤의 행 5개를 보여준다.

 

df.shape()

데이터프레임이 몇행 몇열인지를 (행, 열) 형태로 알려준다.

반응형
Lv. 36 라이츄

Lv. 36 라이츄

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

방명록