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나 분석으로 보내기 전에 키와 값 범위를 검증하라.