EDA 체크리스트

src/content/documents/machine-learning/eda-checklist-for-feature-engineering.json

한 줄 정의

EDA(탐색적 데이터 분석)는 데이터에 질문을 던져 구조·품질·분포·관계를 확인하고 다음 전처리와 특성 공학 결정을 만드는 과정이다.

이 글은 표 데이터를 처음 받았을 때 pandas로 반복 실행할 수 있는 점검 순서를 만든다. 숫자를 출력하는 데서 끝내지 않고 ‘이 결과 때문에 다음에 무엇을 할 것인가’를 연결한다.

실습 데이터 만들기

import pandas as pd
from io import StringIO

csv = StringIO("""customer_id,age,salary,department,channel,purchased
C001,25,3200,Sales,app,0
C002,32,4500,IT,web,1
C003,,6100,HR,store,1
C004,45,7200,IT,web,1
C005,29,3800,Sales,app,0
C006,29,3800,Sales,app,0
""")
df = pd.read_csv(csv)
print(df.shape)
print(df.head(3).to_string(index=False))
(6, 6)
customer_id  age  salary department channel  purchased
       C001 25.0    3200      Sales     app          0
       C002 32.0    4500         IT     web          1
       C003  NaN    6100         HR   store          1

구조: 몇 행·몇 열이며 자료형은 맞는가

shape는 (행 수, 열 수)를 돌려준다. columns는 열 이름을, info()는 자료형·비결측 개수·메모리 요약을 보여 준다.

print("shape:", df.shape)
print("columns:", df.columns.tolist())
df.info()

여기서는 age가 결측값 때문에 float64다. 숫자로 저장됐다고 해서 연속형이라는 뜻은 아니다. purchased는 0/1이지만 목표 범주이고, customer_id는 문자열이지만 일반 범주형 특성으로 바로 쓰기 어렵다. dtype은 저장 형식이고 의미 유형은 분석가가 결정한다.

미리보기: head만 믿지 말자

head()는 앞부분, tail()은 뒷부분, sample()은 임의 행을 보여 준다. 정렬된 데이터에서는 앞 5행만 보면 특정 기간·지역만 보게 될 수 있다.

print(df.head(2))
print(df.tail(2))
print(df.sample(2, random_state=42))

sample은 무작위 표본일 뿐 품질을 증명하지 않는다. 작은 미리보기는 열의 모양과 파싱 오류를 찾는 용도로만 쓰고, 결측·중복·범위는 전체 집계로 확인한다.

요약통계: 중심만 말고 범위와 분포를 본다

describe()는 수치형 열의 개수, 평균, 표준편차, 사분위수, 최솟값, 최댓값을 요약한다. include="all"을 쓰면 범주형 열의 unique, top, freq도 함께 볼 수 있다.

print(df.describe().round(1))
print(df.describe(include="all").loc[["count", "unique", "top", "freq"]])

평균과 중앙값 차이가 크거나 최댓값이 75% 분위수보다 매우 멀면 치우침·이상치를 의심한다. 그러나 업무상 고액 고객일 수도 있으므로 곧바로 삭제하지 않는다. 단위와 수집 규칙을 먼저 확인한다.

결측: 개수와 비율을 함께 본다

missing = pd.DataFrame({
    "count": df.isna().sum(),
    "ratio": df.isna().mean(),
}).query("count > 0").sort_values("ratio", ascending=False)
print(missing)
     count     ratio
age      1  0.166667

6명 중 age 1개가 비어 있으므로 결측률은 약 16.7%다. 다음 질문은 ‘왜 비었는가’다. 무응답인지, 수집 장애인지, 특정 채널에서만 발생하는지 그룹별로 확인한다. 결측 처리 방법은 원인과 모델 목적에 따라 달라진다.

중복: 완전 중복과 업무 키 중복을 나눈다

print("entire duplicate rows:", df.duplicated().sum())
print("duplicate customer ids:", df.duplicated("customer_id").sum())
print("duplicate feature rows:", df.duplicated(
    ["age", "salary", "department", "channel", "purchased"]
).sum())
entire duplicate rows: 0
duplicate customer ids: 0
duplicate feature rows: 1

C005와 C006은 고객 ID만 다르고 나머지가 같다. 서로 다른 실제 고객일 수 있으므로 삭제할 근거가 없다. 주문 데이터라면 주문번호, 고객-날짜 조합처럼 업무상 한 행을 유일하게 만드는 키를 먼저 정의한다.

카디널리티: 범주의 고유값 수를 확인한다

카디널리티는 범주형 변수의 서로 다른 값 개수다. nunique()는 개수를, value_counts()는 값별 빈도를 센다. 기본값에서는 결측값을 제외하므로 결측도 범주처럼 보고 싶다면 dropna=False를 명시한다.

print(df.nunique(dropna=False).sort_values(ascending=False))
print(df["department"].value_counts(dropna=False))
print(df["department"].value_counts(normalize=True, dropna=False).round(3))
customer_id    6
age            5
salary         5
department     3
channel        3
purchased      2
dtype: int64

department
Sales    3
IT       2
HR       1
Name: count, dtype: int64

department
Sales    0.500
IT       0.333
HR       0.167
Name: proportion, dtype: float64

customer_id는 행 수와 고유값 수가 같아 식별자 가능성이 높다. department는 세 범주이며 HR은 한 건뿐이다. 고카디널리티 열을 무조건 삭제할 필요는 없지만, ID 누수·희소한 원-핫 열·새 범주 처리 문제를 검토해야 한다.

범위와 논리 규칙도 코드로 검사한다

checks = {
    "age_out_of_range": (~df["age"].between(0, 120) & df["age"].notna()).sum(),
    "salary_negative": (df["salary"] < 0).sum(),
    "unknown_target": (~df["purchased"].isin([0, 1])).sum(),
}
print(checks)
{'age_out_of_range': 0, 'salary_negative': 0, 'unknown_target': 0}

통계상 드물다는 것과 업무상 불가능하다는 것은 다르다. 나이 -3은 업무 규칙 위반이고, 나이 105는 드물지만 가능할 수 있다. 데이터 사전과 현업 담당자에게 허용 범위·단위·코드를 확인한다.

한 번에 돌리는 EDA 체크리스트

def basic_eda(frame: pd.DataFrame) -> dict:
    return {
        "shape": frame.shape,
        "dtypes": frame.dtypes.astype(str).to_dict(),
        "missing_count": frame.isna().sum().to_dict(),
        "missing_ratio": frame.isna().mean().round(3).to_dict(),
        "nunique_including_na": frame.nunique(dropna=False).to_dict(),
        "duplicate_rows": int(frame.duplicated().sum()),
    }

report = basic_eda(df)
for name, value in report.items():
    print(name, "=>", value)

자동 리포트는 출발점이다. 다음 행동은 사람이 정한다: 자료형 수정, 키 중복 조사, 결측 원인 확인, 희소 범주 통합 검토, 범위 오류 원본 대조 등이다.

누수 없는 EDA 경계

전체 데이터에서 스키마 오류와 데이터 수집 품질을 확인하는 것과, 목표값 관계를 보며 모델 선택에 영향을 주는 탐색을 구분한다.

  • 행·열 수, 파싱 실패, 불가능한 값처럼 모델과 무관한 품질 점검은 전체 수집 파이프라인에서 수행할 수 있다.

  • 목표값과의 관계로 특성을 선택하거나 대치·스케일 규칙을 정하는 일은 학습 데이터 안에서 수행한다.

  • 테스트 세트는 최종 일반화 성능 확인용으로 보존한다. 반복해서 들여다보며 결정을 바꾸면 테스트 세트에도 과적합한다.

  • 교차검증에서는 학습 fold마다 전처기를 다시 fit하도록 Pipeline을 사용한다.

결과를 다음 결정으로 연결하는 표

  • dtype 불일치 → 파싱 규칙과 단위를 확인하고 명시적으로 변환한다.

  • 결측 집중 → 발생 조건을 그룹별로 확인하고 삭제·대치·결측 표시 특성을 비교한다.

  • 고카디널리티 → ID 여부, 빈도, 미지 범주, 인코딩 비용을 검토한다.

  • 희소 범주 → 업무상 합칠 수 있는지 확인하고 ‘기타’ 처리나 규제화를 검토한다.

  • 비정상 범위 → 원천 데이터와 대조해 오류와 실제 희귀 사례를 구분한다.

  • 중복 → 업무 키와 발생 시점을 확인한 뒤 제거·집계 여부를 정한다.

참고 자료

박병선, 『실전 Feature Engineering』 강의 PDF, 10~24쪽 (2026-08-05 확인)

댓글 0

댓글을 불러오는 중…