카테고리 없음

08/03 프로젝트 데이터 분석!

tundercloud 2026. 8. 3. 20:30

1. 오늘의 학습

오늘은 화합물의 구조 정보와 냄새 라벨이 포함된 데이터셋을 머신러닝에 활용하기 전, 데이터의 구조적 품질을 점검하는 작업을 진행했다.

주요 목표는 다음과 같았다.

  • VS Code에서 RDKit 분석 환경 구축
  • CSV 데이터의 구조, 자료형, 결측치 확인
  • SMILES 유효성 검사
  • Canonical SMILES를 이용한 화학구조 표준화
  • 입체화학 정보 확인
  • CID와 분자구조 중복 검증
  • ifra1, ifra2, ifra3 라벨 통합 방법 설계

사용한 데이터는 총 8,806개의 화합물 행과 다음 6개의 원본 컬럼으로 구성되어 있다.

  • smiles
  • cid
  • ifra1
  • ifra2
  • ifra3
  • molblock

분석 과정에서는 원본 행을 추적하기 위한 row_id 컬럼을 추가했다.


2. RDKit 실행 환경 구성

화학구조 분석을 위해 RDKit을 설치한 뒤 다음 코드로 정상 설치 여부를 확인했다.

from rdkit import Chem

print("RDKit 정상 설치")

처음에는 다음 오류가 발생했다.

ModuleNotFoundError: No module named 'rdkit'

원인은 VS Code가 RDKit이 설치된 가상환경이 아닌 다른 Python 인터프리터를 사용하고 있었기 때문이었다.

가상환경을 활성화한 뒤 현재 Python 실행 경로를 확인했다.

.\.venv\Scripts\Activate.ps1
python -c "import sys; print(sys.executable)"

이후 가상환경에 RDKit을 설치하고, VS Code의 Python 인터프리터를 .venv로 변경하여 해결했다.

이번 경험을 통해 패키지 설치 여부만 확인할 것이 아니라, 패키지가 어느 Python 환경에 설치되었는지 확인하는 것이 중요하다는 점을 배웠다.


3. 파일 경로 오류 해결

초기 코드에서 다음과 같이 프로젝트 경로를 설정했다.

PROJECT_DIR = Path.cwd()

INPUT_PATH = (
    PROJECT_DIR
    / "data"
    / "Dataset2_final_w_molblock(2).csv"
)

하지만 파일 존재 여부가 False로 나타났다.

입력 파일 존재 여부: False

이는 CSV 자체의 문제가 아니라 Path.cwd()가 예상한 프로젝트 폴더를 가리키지 않았기 때문이었다.

이를 해결하기 위해 다음과 같은 폴더 구조를 만들었다.

odor_project/
├─ data/
│  └─ Dataset2_final_w_molblock(2).csv
├─ outputs/
└─ structure_check.py

또한 structure_check.py가 위치한 폴더와 상위 폴더를 탐색하여 data 폴더를 자동으로 찾도록 코드를 보완했다.

current_dir = Path.cwd()

search_candidates = [
    current_dir,
    *current_dir.parents,
]

if "__file__" in globals():
    script_dir = Path(__file__).resolve().parent

    search_candidates = [
        script_dir,
        *script_dir.parents,
        *search_candidates,
    ]

이 과정을 통해 분석 코드에서는 데이터 처리뿐 아니라 실행 위치와 파일 경로를 안정적으로 관리하는 것도 재현성의 일부라는 점을 확인했다.


4. 원본 데이터 구조 확인

CSV를 불러올 때 CID와 SMILES, 냄새 라벨을 문자열로 지정했다.

dtype_mapping = {
    "cid": "string",
    "smiles": "string",
    "ifra1": "string",
    "ifra2": "string",
    "ifra3": "string",
    "molblock": "string",
}

df_raw = pd.read_csv(
    INPUT_PATH,
    dtype=dtype_mapping,
    low_memory=False,
)

데이터를 불러온 결과는 다음과 같았다.

전체 행 수: 8,806
원본 컬럼 수: 6
row_id 추가 후 컬럼 수: 7
필수 컬럼 누락 수: 0

원본 데이터프레임은 df_raw로 보존하고, 실제 작업은 복사본인 df에서 진행했다.

df = df_raw.copy()

이는 전처리 과정에서 문제가 생겼을 때 원본 데이터를 다시 불러오지 않고 비교하거나 복원하기 위한 목적이다.


5. 자료형 및 CID 형식 검사

CID는 숫자로 보이지만 계산을 위한 수치형 변수가 아니라 PubChem에서 화합물을 식별하는 번호다.

따라서 CID를 문자열로 통일했다.

df["cid"] = (
    df["cid"]
    .astype("string")
    .str.strip()
    .str.replace(r"\.0$", "", regex=True)
)

이 작업을 수행한 이유는 다음과 같다.

  • "6549""6549.0"을 같은 CID로 처리
  • 앞뒤 공백으로 인한 중복 누락 방지
  • CID 기준 그룹화와 데이터 병합 안정화
  • PubChem CID 조회 오류 방지

숫자가 아닌 형식의 CID도 검사했다.

invalid_cid_format = df.loc[
    df["cid"].notna()
    & ~df["cid"].str.match(r"^\d+$")
]

검사 결과 비정상 형식의 CID는 0건이었다.


6. 결측치 점검

결측치를 확인할 때 단순히 isna()만 사용하지 않고 다음 세 가지를 모두 결측치로 판단했다.

  • NaN
  • 빈 문자열 ""
  • 공백 문자열 " "
def make_missing_mask(series):
    null_mask = series.isna()

    blank_mask = (
        series.astype("string")
        .str.strip()
        .eq("")
        .fillna(False)
    )

    return null_mask | blank_mask

저장 형태는 다르지만 분석 의미는 모두 “유효한 값이 없음”으로 동일하기 때문이다.

결측치 확인 결과는 다음과 같았다.

컬럼결측치 수

row_id 0
smiles 0
cid 0
ifra1 0
ifra2 1,826
ifra3 2,687
molblock 46

ifra2, ifra3의 결측은 반드시 데이터 오류를 뜻하지 않는다. 한 화합물이 하나 또는 두 개의 냄새 라벨만 가질 수 있기 때문이다.

반면 molblock 결측 46건은 3D 구조 기반 분석을 진행할 경우 별도의 처리 기준이 필요하다.


7. 중복 데이터 확인

완전히 동일한 행을 검사한 결과, 중복 그룹에 포함된 행은 8개였고 첫 번째 행을 제외한 제거 후보는 4개였다.

exact_duplicate_mask = df.duplicated(
    subset=original_columns,
    keep=False,
)

추가로 원본 SMILES와 CID 기준 중복도 확인했다.

원본 SMILES 중복 그룹: 15개
원본 SMILES 중복 포함 행: 75개
중복 CID 그룹: 33개
중복 CID 포함 행: 75개

완전 중복과 구조 중복은 서로 다른 개념이다.

  • 완전 중복: 모든 컬럼 값이 동일함
  • 구조 중복: SMILES 또는 Canonical SMILES는 동일하지만 CID, MolBlock 또는 라벨이 다를 수 있음

따라서 중복 데이터는 바로 삭제하지 않고, 구조와 라벨 관계를 먼저 확인해야 한다.


8. SMILES 유효성 검사

SMILES 문자열을 RDKit 분자 객체로 변환해 화학구조로 정상 해석되는지 확인했다.

def smiles_to_mol(smiles):
    if pd.isna(smiles):
        return None

    smiles_text = str(smiles).strip()

    if smiles_text == "":
        return None

    return Chem.MolFromSmiles(smiles_text)

전체 데이터에 적용한 결과는 다음과 같았다.

정상 SMILES: 8,806개
비정상 SMILES: 0개
SMILES 정상 비율: 100.00%

즉, 모든 SMILES가 RDKit에서 정상적인 분자 객체로 변환되었다.

추가로 12개의 분자를 무작위로 선택해 구조 이미지를 생성했다.

molecule_image = Draw.MolsToGridImage(
    sample_molecules,
    legends=sample_legends,
    molsPerRow=4,
)

문자열 유효성 검사뿐 아니라 일부 구조를 사람이 직접 시각적으로 검토하는 과정도 중요하다는 점을 확인했다.


9. Canonical SMILES 생성

같은 분자도 SMILES 작성 순서에 따라 서로 다른 문자열로 표현될 수 있다.

이를 표준화하기 위해 입체화학을 유지한 Canonical SMILES를 생성했다.

df["canonical_smiles"] = (
    df["_mol"]
    .apply(
        lambda mol: Chem.MolToSmiles(
            mol,
            canonical=True,
            isomericSmiles=True,
        )
    )
)

결과는 다음과 같았다.

Canonical SMILES 생성 실패: 0건
Canonical 변환 후 문자열 변경: 6,627건

문자열이 변경된 것은 화학구조가 바뀐 것이 아니라 원자의 작성 순서가 RDKit 기준으로 표준화되었기 때문이다.

고유 구조 수는 다음과 같았다.

고유 원본 SMILES: 8,746개
고유 Canonical SMILES: 8,746개
고유 CID: 8,764개

10. 입체화학 정보 확인

냄새는 입체이성질체에 따라 달라질 수 있으므로 R/S와 E/Z 정보를 제거하지 않고 유지해야 한다.

입체화학을 유지한 SMILES와 제거한 SMILES를 각각 생성했다.

canonical=True
isomericSmiles=True
canonical=True
isomericSmiles=False

입체화학 검사 결과는 다음과 같았다.

검사 항목 행 수
R/S가 지정된 구조 1,016
미지정 입체중심이 있는 구조 2,822
E/Z 또는 Cis/Trans가 지정된 구조 1,176
지정된 입체정보가 있는 구조 2,112
입체정보 제거 시 SMILES가 달라지는 구조 2,119

입체정보를 제거했을 때 고유 구조 수는 7,428개로 감소했다.

입체화학 유지 고유 구조: 8,746개
입체화학 제외 고유 구조: 7,428개

이는 서로 다른 입체이성질체들이 연결 구조만 기준으로 하나의 분자로 합쳐졌기 때문이다.

따라서 냄새 예측 모델에서는 입체화학을 유지한 Canonical SMILES를 기본 구조 식별자로 사용하는 것이 적절하다고 판단했다.


11. 구조와 CID의 관계 확인

Canonical SMILES 기준 중복 구조를 확인한 결과는 다음과 같았다.

중복 Canonical 구조 그룹: 15개
중복 구조에 포함된 행: 75개
구조당 한 행을 유지할 경우 제거 후보: 60개

또한 구조와 CID의 관계를 확인했다.

동일 구조에 여러 CID가 연결된 그룹: 15개
동일 CID에 여러 Canonical 구조가 연결된 사례: 0개
같은 CID·같은 구조이지만 MolBlock이 여러 개인 CID: 29개

동일 CID에 서로 다른 구조가 연결된 사례가 0개라는 점에서 CID와 Canonical 구조 사이에 심각한 매핑 오류는 발견되지 않았다.

반면 같은 CID와 같은 2D 구조지만 여러 MolBlock을 가진 사례는 서로 다른 3D conformer일 가능성이 있다.

따라서 향후 모델링 방식에 따라 처리 기준이 달라진다.

  • 2D fingerprint 모델: 동일 구조 중복 제거 검토
  • 3D 구조 모델: 서로 다른 conformer로 활용 가능

12. 오류 해결: canonical_smiles_no_stereo KeyError

입체화학 결과를 요약하는 과정에서 다음 오류가 발생했다.

KeyError: 'canonical_smiles_no_stereo'

오류의 직접적인 원인은 해당 컬럼을 생성하기 전에 결과 요약 코드에서 사용했기 때문이다.

df["canonical_smiles_no_stereo"]

함수를 정의하는 것만으로는 컬럼이 만들어지지 않는다. 다음 적용 코드까지 실행해야 한다.

df["canonical_smiles_no_stereo"] = (
    df["_mol"]
    .apply(make_non_stereo_canonical_smiles)
    .astype("string")
)

이를 통해 Python 코드는 위에서 아래로 실행되며, 컬럼을 사용하기 전에 반드시 생성 코드가 먼저 실행되어야 한다는 점을 다시 확인했다.

특히 VS Code의 # %% 셀을 개별 실행할 경우 이전 셀을 건너뛰지 않도록 주의해야 한다.


13. 라벨 통합 방법 설계

다음 전처리 단계로 ifra1, ifra2, ifra3를 하나의 다중 라벨 목록으로 통합하는 방법을 설계했다.

예를 들어 다음 데이터를:

ifra1 = Floral
ifra2 = Citrus
ifra3 = Lavender

다음과 같은 리스트로 변환한다.

["Floral", "Citrus", "Lavender"]

라벨 통합 시 적용할 기준은 다음과 같다.

  1. NaN, 빈 문자열, 공백 문자열 제외
  2. 라벨 앞뒤 공백 제거
  3. 대소문자 표기 통일
  4. 같은 행에서 중복된 라벨 제거
  5. ifra1 → ifra2 → ifra3 순서 유지
  6. 정제 전 라벨과 정제 후 라벨 모두 보존

현재 데이터에서는 원본 표기 기준 213개의 라벨이 존재했으며, 대소문자를 통일하면 약 200개로 감소하는 것으로 확인했다.

예:

Floral / floral
Green / green
Woody / woody

통합 함수는 다음과 같은 형태로 설계했다.

def combine_clean_labels(row):
    labels = []
    seen = set()

    for column in [
        "ifra1_clean",
        "ifra2_clean",
        "ifra3_clean",
    ]:
        value = row[column]

        if pd.isna(value):
            continue

        label = str(value).strip()
        key = label.casefold()

        if key in seen:
            continue

        seen.add(key)
        labels.append(label)

    return labels

향후 최종 타깃 컬럼은 다음과 같이 구성할 예정이다.

df["labels_clean"]

14. 추가로 확인해야 할 라벨 문제

라벨을 통합할 때 Odorless와 다른 냄새 라벨이 동시에 존재하는 사례도 확인해야 한다.

예:

["Odorless", "Fruity"]

Odorless는 무취를 의미하지만 다른 냄새 라벨과 함께 기록될 경우 의미상 충돌할 수 있다.

다만 농도, 실험 조건, 평가자 차이 등이 원인일 수 있으므로 바로 삭제하지 않고 다음과 같이 플래그를 추가할 예정이다.

df["odorless_conflict"] = (
    df["labels_clean"]
    .apply(
        lambda labels:
        "Odorless" in labels
        and len(labels) > 1
    )
)

이후 충돌 행을 별도로 검토해 유지, 수정 또는 제외 여부를 결정할 계획이다.


15. 오늘의 핵심 결과

점검 항목 결과
전체 행 수 8,806
필수 컬럼 누락 0
비정상 CID 0
MolBlock 결측 46
완전 중복 제거 후보 4
정상 SMILES 비율 100%
Canonical 생성 실패 0
고유 Canonical 구조 8,746
고유 CID 8,764
입체정보 보유 구조 2,112
입체정보 제거 시 변경 구조 2,119
중복 Canonical 구조 그룹 15
동일 CID·복수 구조 0
동일 CID·동일 구조·복수 MolBlock 29