Skip to content

Pandas Data Cleaning: 실무 워크플로우

Updated on

지저분한 데이터는 분석을 초기에 멈춥니다. 즉흥적인 fillna나 문자열 치환은 잘못된 타입, 조용한 중복, 이상치로 인한 왜곡을 숨깁니다.

PAS 관점:

  • 문제: 타입 혼합, 결측, 중복이 조인을 망치고 통계를 왜곡한다.
  • 심화: 땜질식 수정은 문제를 뒤로 미뤄 재작업을 만든다.
  • 해결: 반복 가능한 체크리스트로 타입을 맞추고, 결측을 의도적으로 처리하며, 이상치와 중복을 정리하고 마지막에 검증한다.

워크플로우 요약

단계할 일예시
1. 프로파일컬럼/결측/유니크 확인df.info(), df.describe(include="all")
2. 컬럼 정규화이름/텍스트 정리df.columns = df.columns.str.strip().str.lower()
3. 타입 교정to_datetime, to_numeric, astype("category")df["date"] = pd.to_datetime(df["date"], errors="coerce")
4. 결측 처리규칙에 따라 삭제/채움df["age"] = df["age"].fillna(df["age"].median())
5. 이상치 처리클립/플래그df["rev_cap"] = df["revenue"].clip(upper=df["revenue"].quantile(0.99))
6. 중복 제거완전/부분 중복 삭제df.drop_duplicates(subset=["id", "date"], keep="last")
7. 검증범위/카테고리 확인assert df["score"].between(0,100).all()

1) 빠르게 프로파일

summary = {
    "rows": len(df),
    "columns": df.shape[1],
    "nulls": df.isna().sum(),
    "unique_counts": df.nunique(),
}
  • df.info()로 타입 혼합을 조기에 발견.
  • df.describe(percentiles=[0.01,0.99])로 분포 왜곡 확인.

2) 컬럼과 텍스트 정리

df.columns = df.columns.str.strip().str.lower().str.replace(" ", "_")
df["country"] = df["country"].str.strip().str.title()
  • 표준화된 이름은 조인/코드 단순화.
  • 특이 문자에는 .str.normalize("NFKC") 활용.

3) 데이터 타입 수정

df["date"] = pd.to_datetime(df["date"], errors="coerce", utc=True)
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
df["segment"] = df["segment"].astype("category")
  • errors="coerce"로 잘못된 값을 NaT/NaN으로 드러냄.
  • convert_dtypes()로 일괄 nullable/메모리 절약형 타입 적용.

4) 결측값 처리

df["age"] = df["age"].fillna(df["age"].median())
df["city"] = df["city"].fillna("Unknown")
df = df.dropna(subset=["id"])  # 필수 키
  • 열별 전략: 숫자→중앙값/평균, 카테고리→최빈/플레이스홀더.
  • 신호 보존: df["age_imputed"] = df["age"].isna().

5) 이상치 다루기

upper = df["revenue"].quantile(0.99)
lower = df["revenue"].quantile(0.01)
df["revenue_capped"] = df["revenue"].clip(lower=lower, upper=upper)
  • 비율은 Z-스코어로 필터링 가능 (abs(z) < 3 등).
  • 금액 데이터는 삭제보다 클리핑이 건수 보존에 유리.

6) 중복 안전하게 제거

df = df.drop_duplicates(subset=["id", "date"], keep="last")
  • 유일 키 확인: assert df.duplicated(["id"]).sum() == 0.
  • 퍼지 매칭이 필요하면 먼저 정규화(예: 이메일 소문자화).

7) 내보내기 전 검증

assert df["score"].between(0, 100).all()
valid_segments = {"basic", "pro", "enterprise"}
assert df["segment"].isin(valid_segments).all()
  • 테스트에서는 pd.testing.assert_frame_equal로 출력 비교 가능.
  • 파이프라인에 가벼운 행수/결측 체크를 넣어 회귀를 방지.

엔드투엔드 미니 파이프라인

def clean(df):
    df = df.copy()
    df.columns = df.columns.str.strip().str.lower()
    df["date"] = pd.to_datetime(df["date"], errors="coerce")
    df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
    df["amount"] = df["amount"].fillna(df["amount"].median())
    df["segment"] = df["segment"].fillna("unknown").str.lower()
    df = df.dropna(subset=["id"])
    df = df.drop_duplicates(subset=["id", "date"], keep="last")
    assert df["amount"].ge(0).all()
    return df
  • 먼저 복사해 입력을 변형하지 않는다.
  • df.pipe(clean)으로 읽기 쉬운 파이프라인을 유지.

관련 가이드


핵심 정리

  • 이름을 표준화하고 타입을 강제하며 결측을 의도적으로 처리하라.
  • 이상치는 조용히 삭제하기보다 클립/플래그로 다뤄라.
  • BI나 분석으로 보내기 전에 키와 값 범위를 검증하라.