본문 바로가기
데이터 분석

[데이터 분석] 신약 개발을 위한 화합물 독성 예측 프로젝트(EDA)

by 클레어몬트 2025. 5. 1.

https://claremont.tistory.com/entry/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%84%9D-%EB%B6%84%EC%84%9D-%EA%B8%B0%EB%B2%95-%EB%B0%8F-%ED%99%9C%EC%9A%A9

 

[데이터 분석] 분석 기법 및 활용

데이터 분석 기법에 대해 알아보자정형 데이터 분석 / 비정형 데이터 분석으로 구분되며, 각각의 기법이 특정한 데이터 유형과 분석 목적에 따라 활용된다! [정형 데이터 분석]- 탐색적 데이터

claremont.tistory.com

ㅁEDA(탐색적 데이터 분석): 모델을 만들기 전에 데이터를 파악하고 이해하기 위해 통계적 기법과 시각화 등을 통해 데이터를 분석하는 과정

 

 

 

(raw 데이터 파일)

train.csv

 

[변수 설명] - 수치형 변수 4가지

이름 의미 활용 해석
MolWt(Molecular Weight) 분자의 전체 질량 약물의 흡수 분포, 대사 등에 영향을 미침 ◦ 너무 무거운 분자는 세포막 투과력이 낮음.
◦ Lipinski’s Rule에 따르면 500 g/mol 이하가 경구 약물에 적합.
clogp(Calculated LogP) 지질/수분 분배계수의 로그값 (logP), 소수성(Hydrophobicity)측정 약물이 지질막(세포막)을 통과할 수 있는지 평가할 때 사용됨.

• 값이 클수록 지질에 잘 녹고 작을수록, 수용성이 높음
sa_score(Synthetic Accessibility Score) 합성 가능성 점수 (Synthetic Accessibillity) 실제 실험실에서 약물을 합성할 수 있는지를 판단할 때 사용.

◦ 점수가 낮을수록 합성이 쉽고, 높을수록 합성이 어려움.
◦ 일반적으로 1(쉬움) ~ 10(어려움) 사이의 값을 가짐.
qed(Quantitative Estimate of Drug-likeness) 약물 적합성에 대한 종합 점수 여러 물리화학적 특성을 통합하여 후보 약물의 이상적 특성 여부를 판단.

• 0.9 이상: 매우 우수한 약물 후보
• 0.5 이상: 일반적인 수준
• 0.3 이하: 적합하지 안흘 가능성 높음

 

 

 

 

[종속변수 value-count]

label 변수는 마치 boolean 자료형과 같다

 

앞서 언급했듯이, 데이터셋의 종속변수 label은 화합물의 독성 여부를 나타내며, 0은 독성이 있는 화합물, 1은 독성이 없는 화합물을 의미한다.

Count Plot을 통해 label의 분포를 확인한 결과, 두 클래스 간의 샘플 수는 약간의 차이는 있으나 전반적으로 균형 잡힌 분포를 보였다.

이는 클래스 불균형 문제가 존재하지 않음을 의미하며, 모델 학습 시 특정 클래스에 편향되거나, 성능 지표(정확도, 정밀도 등)가 왜곡될 가능성이 낮다. 따라서 별도의 리샘플링 기법(SMOTE, 언더샘플링 등)을 적용하지 않고도, 모델이 양 클래스 모두에 대해 균형 잡힌 학습과 일반화 성능을 낼 수 있다 볼 수 있다.

 

 

 

SMILES 문자열 변수

[ rdkit ] - 화학정보학 오픈소스 파이썬 라이브러리

from rdkit import Chem
from rdkit.Chem import Descriptors

# SMILES로 분자 생성
mol = Chem.MolFromSmiles('CC(=O)OC1=CC=CC=C1C(=O)O')  # 아스피린

# 분자량 계산
print(Descriptors.MolWt(mol))  # → 180.16

# 지용성(LogP)
print(Descriptors.MolLogP(mol))  # → 1.19

 

이렇게 분자식 구조를 입력하면 분자량 계산과 지용성 계산에 쉽게 활용할 수 있다

 

 

Bit 서브구조 추출

서브 구조로 SMILE CODE를 쪼갤 수 있으므로, 독성이 있는 물질을 대상으로 가장 많이 등장한 서브 구조를 추출.

추출한 것 중 TOP 8 리스트를 만들어서(독성 물질에서 많이 등장한 서브구조 추출) 해당 값들이 몇 번 등장하는지 추출!

 

예시) 아래는 train 데이터의 Top 8 리스트 서브 구조를 시각화 한 자료. 첫 번째 row의 SMILES 값은 ‘Nc1ccncc1’으로, 이를 구성하는 서브 구조는 다음과 같다. 아래 리스트에 3번 등장하므로 값이 3이 되는 것이다.

  • 서브 구조
    • bit 147: atom index: 0, radius: 0
    • bit 184: atom index: 3, radius: 2
    • bit 356: atom index: 5, radius: 2 
    • bit 378: atom index: 1, radius: 0 
    • bit 383: atom index: 4, radius: 0
    • bit 433: atom index: 4, radius: 2
    • bit 439: atom index: 0, radius: 1
    • atom index: 2, radius: 2
    • bit 579: atom index: 6, radius: 2
    • bit 726: atom index: 4, radius: 1
    • bit 780: atom index: 6, radius: 1
    • bit 842: atom index: 1, radius: 2
    • bit 849: atom index: 5, radius: 1
    • bit 888: atom index: 6, radius: 0

bit 356, bit 726, bit 849 3개가 TOP 8 리스트에 있으므로 cnt_top_bit 값이 3이 된다

 

# 1. 독성 있는 데이터만 필터링
toxic_df = df[df['label'] == 1]

# 2. 각 분자의 fingerprint 추출하여 bit 카운트
bit_counter = Counter()
bit_mapping = {}

for smi in toxic_df['SMILES']:
    mol = Chem.MolFromSmiles(smi)
    bitInfo = {}
    fp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=1024, bitInfo=bitInfo)
    bit_counter.update(fp.GetOnBits())
    for bit in fp.GetOnBits():
        if bit in bit_mapping:
            continue
        if bit in bitInfo:
            bit_mapping[bit] = (mol, bitInfo[bit][0])  # 중심 원자, 반경

# 3. 상위 8개 자주 등장한 bit 추출
top_bits = [bit for bit, _ in bit_counter.most_common(8)]

# 4. 각 SMILES가 top bit를 몇 개 포함하는지 세기
def count_top_bits(smi, top_bits):
    mol = Chem.MolFromSmiles(smi)
    if mol is None:
        return 0
    bitInfo = {}
    fp = AllChem.GetMorganFingerprintAsBitVect(mol, radius=2, nBits=1024, bitInfo=bitInfo)
    return sum(1 for bit in top_bits if bit in fp.GetOnBits())

# 5. 결과 컬럼 추가
df['cnt_top_bit'] = df['SMILES'].apply(lambda x: count_top_bits(x, top_bits))

 

 

 

Finger Print

1024 비트 변수 생성 : ECFP, FCFP, PTFP

1024개씩 인덱스를 끊어서 3개의 변수로 정리

# 열 인덱스 기준으로 합산
ecfp = df.iloc[:, 1:1025].astype(str).agg(''.join, axis=1)
fcfp = df.iloc[:, 1025:2049].astype(str).agg(''.join, axis=1)
ptfp = df.iloc[:, 2049:3073].astype(str).agg(''.join, axis=1)

 

파생 변수 추가 : PCA 차원 축소

1024차원을 2차원으로 축소해서 시각화

ECFP: Extended-Connectivity Finger Print
FCFP: Functional-Class Finger Print
PTFP: PaTtern Finger Print

 

 

PCA로 압축된 값을 파생변수로 추가

from sklearn.decomposition import PCA

# 예: 1024차원 fingerprint matrix (예: ecfp_matrix)
# → 여기선 2차원 축소를 가정
pca = PCA(n_components=2)
pca_result = pca.fit_transform(ecfp_matrix)

# 설명된 분산 비율 (각 성분의 중요도)
weights = pca.explained_variance_ratio_

# 가중 합산: 각 성분에 가중치 곱해서 1개 값으로 합치기
pca_weighted_feature = (
    pca_result[:, 0] * weights[0] +
    pca_result[:, 1] * weights[1]
)

# 결과를 DataFrame에 추가
df['ecfp_pca_feature'] = pca_weighted_feature

 

FCFP와 PTFP도 마찬가지로 위와 동일한 코드로 파생변수 추가

 

 

+ 파생 변수 추가 : fingerprint별 활성 비트 수

PTFP는 ECFP, FCFP보다 더 많은 비트가 활성화되었고 이는 잠재적으로 더 많은 구조적 또는 물리화학적 정보를 담고 있을 수 있음

→ fingerprint별 활성 비트 수 파생 변수 추가하기로 결정

# 각 행(샘플)별로 1의 개수를 세어 파생 변수로 추가
df['ecfp_bit_count'] = ecfp_matrix.sum(axis=1)
df['fcfp_bit_count'] = fcfp_matrix.sum(axis=1)
df['ptfp_bit_count'] = ptfp_matrix.sum(axis=1)

 

 

 

 

 

[수치형 변수 분석(왜도, 이상치 등)]

 

1. MolWt

fig, axes = plt.subplots(1, 2, figsize=(10, 3))

# Boxplot
sns.boxplot(data=df, x='label', y='MolWt', ax=axes[0])
axes[0].set_title('Boxplot')

# KDEplot
sns.kdeplot(data=df, x='MolWt', hue='label', fill=True, common_norm=False, ax=axes[1], palette='Set2')
axes[1].set_title('KDEplot')

plt.tight_layout()
plt.show()
# IQR 계산
Q1 = df['MolWt'].quantile(0.25)
Q3 = df['MolWt'].quantile(0.75)
IQR = Q3 - Q1

# 이상치 조건
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 이상치 필터링
outliers = df[(df['MolWt'] < lower_bound) | (df['MolWt'] > upper_bound)]

# 이상치 개수 출력
print("이상치 개수:", len(outliers))

# 결과값 
이상치 개수: 130

오른쪽은 단순 MolWt 데이터에 대한 분포를 나타내고, 왼쪽은 label 변수값에 따른 분포를 나타낸다

 

 

[MolWt 시각화 해석]

1. Boxplot 해석 두 클래스(label=0: Toxic, label=1: Non-toxic)의 중앙값은 유사하지만, Toxic(0)은 상대적으로 낮은 clogP 값을 더 많이 포함하고 있음 이상치(outlier)는 양쪽 모두 존재하나, Toxic 클래스에서 낮은 값 쪽 이상치가 더 많음

 

2. KDEplot 해석 Toxic(0) 클래스는 2~4 범위에 밀집 Non-toxic(1) 클래스는 4~6 이상으로 더 오른쪽으로 분포 두 클래스 간 분포의 밀도와 중심 위치가 명확하게 다름 → clogP가 높을수록 Non-toxic일 가능성 높음 → clogP가 낮을수록 Toxic일 가능성 높음

 

 

 

2. clogp

(코드 플로우 위와 동일)

(코드 플로우 위와 동일)

(시각화 그래프 설명 위와 동일)

 

 

[clogp 시각화 해석]

1. Boxplot 해석 두 클래스(label=0: Toxic, label=1: Non-toxic)의 중앙값은 유사하지만, Toxic(0) 쪽은 낮은 값 구간(0~2)에 이상치가 더 많이 분포함 전체적으로 Non-toxic(1) 클래스의 clogP 값은 조금 더 분산 폭이 좁고 높은 위치에 있음

 

2. KDEplot 해석 Toxic(0) 클래스는 clogP가 2~4 사이에 분포 밀도가 높음 Non-toxic(1) 클래스는 4~6 사이에 더 뚜렷하게 집중되어 있음 분포의 중심값 위치가 명확히 다르며, → clogP 값이 클수록 Non-toxic일 가능성이 높아지는 경향 확인됨

 

 

 

3. sa_score

(코드 플로우 위와 동일)

(코드 플로우 위와 동일)

(시각화 그래프 설명 위와 동일)

 

[sa_score 시각화 해석]

1. Boxplot 해석 - 두 집단(label=0, 1)의 sa_score 중앙값은 비슷하지만, 독성 없는 화합물은 약간 더 낮은 중앙값을 가진다. - 두 그룹 모두 이상치가 존재한다 특히 label = 1 쪽에서 극단적으로 낮은 sa_score가 소수 나타난다. - 전반적으로 sa_score가 2~4 사이에 집중된 분포를 보인다.

 

2. KDEplot 해석 - 독성 없는 그룹의 분포가 조금 더 왼쪽으로 이동되어 있다. -> 합성하기 쉬운 화합물이 상대적으로 많다 - 독성 있는 그룹은 분포가 더 넓다. -> 구조가 복잡하고 합성이 어려운 경우도 존재함을 암시한다.

 

 

 

4. qed

(코드 플로우 위와 동일)

(코드 플로우 위와 동일)

(시각화 그래프 설명 위와 동일)

 

[qed 시각화 해석]

1. Boxplot 해석 - 독성 없는 화합물(label=1)이 더 높은 중앙값을 보인다. -> 약물 적합성이 평균적으로 더 우수함

 

2. KDEplot 해석 - label=1의 분포는 우측(0.6)이상 에 더 많은 밀도가 몰려 있다. - label=0은 상대적으로 좌측(0.3-0.5)에 집중되어 있다 -> qed 분포자체가 독성 여부에 따라 뚜렷한 차이를 보인다.

 

 

 

 

변수 간 상관관계 및 다중공선성

1. 상관관계(Correlation): 두 변수 간의 선형적 관계를 나타내는 통계적 척도

피어슨 상관계수 (-1 ~ +1) 사용. 수치형 변수 간의 상관관계 파악

if target_var in df.columns:
    print(f"\n[6. {target_var} 변수와의 상관관계 분석]")
    try:
        # 상관계수 계산
        cols_for_corr = numerical_vars + [target_var]
        correlation_matrix = df[cols_for_corr].corr()
        correlation_with_target = correlation_matrix[target_var].sort_values(ascending=False)
        print("\n상관계수 (vs {}) :".format(target_var))
        print(correlation_with_target)

        # 히트맵 시각화
        plt.figure(figsize=(8, 6))
        sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', fmt=".2f")
        plt.title(f'Correlation Heatmap including {target_var}')
        plt.show() # 플롯을 셀 출력으로 표시
    except Exception as e:
        print(f"\n - 상관관계 계산 중 오류 발생 (데이터 타입 확인 필요): {e}")
else:
    print(f"\n[6. '{target_var}' 컬럼을 찾을 수 없어 상관관계 분석을 생략합니다.]")

선형적 관계를 나타내는 히트맵

 

 

2. 다중공선성(Multicollinearity): 독립변수들 간의 강한 상관관계 존재하는 것(한 독립변수가 다른 독립변수들의 선형 조합으로 설명 가능)

 

(발생하는 문제점)

  • 모델 계수의 불안정성
  • 변수 영향력 해석의 어려움
  • 모델 성능 저하 가능성

분산 팽창 지수(VIF) 계산

from statsmodels.stats.outliers_influence import variance_inflation_factor
import pandas as pd

print("\n[다중공선성 확인 (VIF)]")
X = df[numerical_vars].dropna()
vif_data = pd.DataFrame()
vif_data["feature"] = X.columns
vif_data["VIF"] = [variance_inflation_factor(X.values, i) for i in range(len(X.columns))]

print(vif_data)
    feature        VIF
0     MolWt  34.436728
1     clogp   9.965039
2  sa_score  27.374073
3       qed   6.304276

--- VIF 해석 ---
VIF = 1: 다중공선성 없음
1 < VIF < 5: 약한 다중공선성 의심
5 <= VIF < 10: 다소 높은 다중공선성 의심
VIF >= 10: 높은 다중공선성, 조치 고려

 

MolWt와 sa_score의 다중공선성이 의심되기 때문에 분석 모델 구축 시 유의할 필요가 있음.

전처리 과정에서 손을 보거나, 심한 경우에는 피처값을 제거하는 경우도 고려!