머신러닝 실전 워크플로 (4) - EDA와 데이터 품질 자동 점검
탐색적 분석으로 데이터의 분포와 결측, 이상을 한 번 점검하는 것과, 그 점검을 pandera 같은 도구로 파이프라인에 넣어 새 데이터가 들어올 때마다 자동으로 검사하는 것의 차이를 정리한 4편입니다.
머신러닝 실전 워크플로 시리즈의 4편입니다. 3편의 “데이터 수집과 버전관리”에 이어집니다.
데이터를 믿어도 되는가
3편에서 데이터를 모으고 버전을 붙였습니다. 파일이 손에 있다고 해서 그 데이터가 옳다는 보장은 없습니다. 이번 편은 모델에 넣기 전에 “이 데이터를 신뢰할 수 있는가”를 확인하는 단계입니다.
두 가지 일을 합니다. 하나는 데이터의 모양을 눈으로 파악하는 탐색적 분석(EDA)이고, 다른 하나는 거기서 알아낸 사실을 코드로 못 박아 새 데이터가 들어올 때마다 자동으로 검사하는 품질 점검입니다. 앞은 사람이 한 번 하는 일이고, 뒤는 파이프라인이 매번 하는 일입니다.
머신러닝 기초 13편에서 검증 점수가 조용히 부풀려질 수 있다고 했습니다. 점수만이 아니라 데이터 자체도 조용히 오염될 수 있습니다. 오염된 데이터 위에 세운 모델은 아무리 잘 튜닝해도 무너집니다. 데이터를 믿을 수 있는지를 먼저 묻는 이유입니다.
EDA: 데이터의 첫 점검
EDA는 세 가지 질문에 답합니다.
- 분포: 각 열이 어떤 값을 어떤 빈도로 갖는가. 한쪽으로 심하게 치우쳤거나 예상 밖의 값이 섞였는가.
- 결측: 어디가 얼마나 비었는가. 결측이 무작위인가, 특정 조건에서만 생기는가.
- 이상: 물리적으로나 논리적으로 말이 안 되는 값이 있는가. 음수 요금, 200세, 미래 날짜 같은 것들.
대표 도구 하나로 확인하면 pandas의 몇 줄이면 첫 점검이 끝납니다.
1
2
3
4
5
6
7
8
9
import pandas as pd
df = pd.read_parquet("data/raw/trips.parquet")
df.shape # (행, 열) 크기부터 확인
df.info() # 열별 dtype와 non-null 개수
df.isna().mean().sort_values(ascending=False) # 열별 결측 비율, 높은 순
df.describe() # 수치 열의 min/max/사분위수
df["city"].value_counts() # 범주 열의 분포와 예상 밖 값
각 줄이 위 세 질문에 대응합니다. describe()의 min과 max는 이상값의 첫 단서입니다. fare의 min이 음수면 그 자리에서 문제가 드러납니다. value_counts()는 스키마에 없던 카테고리를 잡아내고, isna().mean()은 결측이 특정 열에 몰려 있는지를 보여줍니다. 분포는 df.hist()로 한눈에 훑고, 치우친 열은 로그를 씌워 다시 봅니다.
이 단계를 구체 데이터로 끝까지 밀어붙인 예는 NYC 택시 파이프라인의 데이터/EDA 편에 있습니다.
일회성 EDA로는 부족하다
EDA는 한 시점의 스냅샷을 사람이 눈으로 본 결과입니다. 노트북에 리포트를 남기고 끝납니다. 문제는 데이터가 한 번 오고 마는 게 아니라는 점입니다.
다음 배치, 다음 달 데이터에서는 새 카테고리가 생기고, 결측률이 뛰고, 단위가 미터에서 킬로미터로 바뀝니다. 오늘 눈으로 “age는 0에서 120 사이”를 확인해도, 그 사실을 코드로 적어두지 않으면 내일 age가 -5로 들어와도 파이프라인은 조용히 통과시킵니다. EDA가 알아낸 것과 파이프라인이 강제하는 것 사이에 틈이 생기고, 사고는 그 틈에서 납니다.
그래서 EDA에서 알아낸 사실을 검사 규칙으로 옮겨 파이프라인에 넣어야 합니다. 사람이 매번 노트북을 다시 열어 확인할 수는 없기 때문입니다.
기대를 코드로: assertion
가장 단순한 형태는 assert입니다. EDA에서 알아낸 사실을 그대로 조건문으로 옮깁니다.
1
2
3
4
5
6
7
8
def check_quality(df):
assert df["age"].between(0, 120).all(), "age가 0~120 범위를 벗어남"
assert df["fare"].gt(0).all(), "fare에 0 이하 값이 있음"
assert df["city"].isin(["Seoul", "Busan", "Daegu"]).all(), "미등록 city"
assert df["user_id"].is_unique, "user_id 중복"
na_rate = df["age"].isna().mean()
assert na_rate < 0.05, f"age 결측률 {na_rate:.1%}가 허용치 초과"
return df
이 함수를 데이터 적재 직후에 부르면, 기대를 어긴 데이터는 모델에 닿기 전에 멈춥니다. 핵심은 조용한 오염을 시끄러운 에러로 바꾸는 것입니다. 늦게 드러날 문제를 가장 이른 지점에서 잡아, 잘못된 데이터로 학습이 끝난 뒤에 원인을 되짚는 일을 없앱니다.
에러를 내지 않고 통과하는 데이터 문제가 가장 위험하다. 코드는 잘 돌고 학습도 끝나지만, 결과가 조용히 틀린다. assertion은 “말이 안 되는 데이터는 여기서 멈춘다”를 명시해 이 침묵을 깬다.
스키마로 선언하기: pandera
assert를 늘어놓다 보면 곧 한계가 옵니다. 열이 수십 개면 조건이 코드 여기저기 흩어지고, 무엇이 왜 실패했는지 메시지를 일일이 적어야 합니다. pandera는 데이터에 대한 기대를 하나의 스키마로 선언해 이 문제를 정리합니다.
1
2
3
4
5
6
7
8
9
10
import pandera as pa
from pandera import Column, Check, DataFrameSchema
schema = DataFrameSchema({
"age": Column(int, Check.in_range(0, 120), nullable=False),
"city": Column(str, Check.isin(["Seoul", "Busan", "Daegu"])),
"fare": Column(float, Check.gt(0)),
})
schema.validate(df, lazy=True) # 통과 시 df 반환, 실패 시 모든 위반을 SchemaErrors로 보고
dtype, 값 범위, 허용 카테고리, nullable 여부를 한곳에 모아 선언합니다. lazy=True는 첫 위반에서 멈추지 않고 위반을 모두 모아 한 번에 보고하므로, 새 데이터의 어디가 얼마나 어긋났는지 전체를 봅니다. 스키마는 검사기이면서 문서이기도 합니다. 이 데이터가 어떤 모양이어야 하는지가 코드로 남아, 다음 사람이 데이터 규격을 코드에서 바로 읽습니다.
더 큰 파이프라인: Great Expectations
Great Expectations는 같은 문제를 더 큰 규모로 풉니다. 기대(expectation)를 모아 하나의 suite로 만들고, 검증 결과를 사람이 읽는 품질 리포트로 남깁니다. 기대의 이름 자체가 서술적입니다.
expect_column_values_to_be_between("age", 0, 120)expect_column_values_to_not_be_null("user_id")expect_column_values_to_be_in_set("city", ["Seoul", "Busan", "Daegu"])
pandera가 코드 안에 두는 가벼운 스키마라면, Great Expectations는 팀이 공유하는 품질 문서와 검증 이력에 무게를 둡니다. 데이터셋이 크고 검증 결과를 여러 사람이 함께 봐야 하는 파이프라인에 어울립니다.
도구는 다르지만 푸는 문제는 하나입니다. 데이터에 대한 기대를 명시적으로 적어두고, 새 데이터가 들어올 때마다 자동으로 검사한다는 것입니다. assertion, pandera, Great Expectations는 이 한 가지를 각각 다른 무게로 구현한 것입니다. 작게 시작해서 파이프라인이 커지면 도구를 올리면 됩니다.
여기서 놓치면 드리프트가 된다
품질 점검은 학습 때 세운 데이터의 전제를 서빙 시점에도 지키게 하는 첫 방어선입니다. 여기서 잡지 못한 문제는 사라지지 않고 뒤로 미뤄집니다.
분포가 서서히 변하거나 새 카테고리가 꾸준히 늘면, 파이프라인은 에러 없이 돌지만 모델 성능이 조용히 내려갑니다. 이것이 데이터 드리프트입니다. 오늘 pandera 스키마에 적어둔 기대는 나중에 그대로 모니터링의 점검 항목이 됩니다. 학습 시점의 분포를 기준으로 서빙 데이터가 얼마나 벗어났는지 재는 것이 드리프트 감시이기 때문입니다. 이번 편에서 데이터의 기대를 잘 적어둘수록 뒤에서 감시할 대상이 분명해집니다.
드리프트와 재학습은 12편에서 다룹니다. 다음 편은 이렇게 신뢰를 확인한 데이터를 재현 가능한 피처로 바꾸는 피처 파이프라인입니다.
정리
| 개념 | 한 줄 요약 |
|---|---|
| EDA | 분포, 결측, 이상 세 질문으로 데이터를 신뢰할 수 있는지 사람이 한 번 점검한다 |
| 첫 점검 루틴 | shape, info, isna, describe, value_counts로 pandas 몇 줄에 첫 상태를 훑는다 |
| 일회성 EDA의 한계 | 스냅샷 한 장일 뿐, 새 데이터의 변화를 잡지 못한다 |
| assertion | EDA에서 알아낸 사실을 조건문으로 옮겨 오염을 이른 지점에서 에러로 만든다 |
| pandera | 기대를 하나의 스키마로 선언한다. 검사기이자 문서다 |
| Great Expectations | 기대를 suite로 모으고 품질 리포트로 남긴다. 크고 협업적인 파이프라인에 |
| 드리프트로의 연결 | 여기서 적어둔 기대가 뒤에 모니터링의 점검 항목이 된다 |
여기서 데이터의 기대를 코드로 남겨두면, 다음 편의 피처 파이프라인이 그 위에서 안전하게 돌아갑니다.