포스트

머신러닝 실전 워크플로 (3) - 데이터 수집과 버전관리

파일과 DB와 API에서 데이터를 받아 원본을 불변으로 보존하는 원칙, 수집 시점의 스키마 검증과 데이터 계약, 그리고 모델 재현에 코드와 데이터의 버전이 모두 필요한 이유와 DVC의 최소 사용법을 정리한 3편입니다.

머신러닝 실전 워크플로 (3) - 데이터 수집과 버전관리

머신러닝 실전 워크플로 시리즈의 3편입니다. 2편의 “프로젝트 구조와 재현성”에 이어집니다.

코드만 버전관리하면 절반이다

2편에서 프로젝트 구조와 재현성을 세웠습니다. 같은 코드, 같은 환경, 같은 시드면 결과가 재현된다고 했습니다. 그런데 여기엔 조용한 전제가 하나 있습니다. 데이터가 그때와 똑같다는 전제입니다.

실무 데이터는 코드보다 자주 바뀝니다. 소스에 행이 추가되고, 잘못된 값이 뒤늦게 수정되고, 컬럼이 바뀝니다. 3개월 전 커밋을 git checkout으로 되돌려도, 그때 학습에 쓴 데이터가 지금 손안에 없으면 그 모델은 재현되지 않습니다. 재현에는 코드의 버전과 데이터의 버전이 둘 다 필요합니다. 이 편은 데이터를 어디서 어떻게 받아 원본으로 보존하고, 수집 경계에서 검증하고, 코드처럼 버전으로 묶는지를 다룹니다.

데이터 소스와 원본 보존

데이터는 크게 세 곳에서 옵니다. 파일(CSV, Parquet 등), 데이터베이스(SQL 쿼리), API(HTTP 응답)입니다. 소스가 무엇이든 수집의 첫 규칙은 하나입니다. 받은 원본을 그대로 저장하고, 그 뒤로 절대 덮어쓰지 않습니다.

이유는 원본이 유일한 진실의 기준이기 때문입니다. 전처리 로직에 버그가 있었다면 원본에서 다시 만들면 됩니다. 그러나 원본을 이미 가공된 값으로 덮어썼다면 되돌릴 곳이 없습니다. 그래서 데이터 폴더를 단계로 나눕니다.

  • data/raw: 소스에서 받은 그대로. 읽기 전용으로 취급하고 어떤 스크립트도 여기에 쓰지 않는다.
  • data/interim: 중간 산출물. 파싱, 병합처럼 되돌릴 수 있는 변환의 결과.
  • data/processed: 모델이 바로 입력으로 받는 최종 형태.

파이프라인은 언제나 raw를 입력으로 읽고 processed를 출력으로 씁니다. raw는 입력일 뿐 출력이 아닙니다.

1
2
3
4
5
6
7
8
9
from pathlib import Path
import pandas as pd

RAW = Path("data/raw")
RAW.mkdir(parents=True, exist_ok=True)

# DB에서 한 달치를 쿼리해 받은 그대로 저장한다. 여기서 가공하지 않는다
df = pd.read_sql("SELECT * FROM trips WHERE month = '2025-01'", conn)
df.to_parquet(RAW / "trips_2025-01.parquet")   # 원본 스냅샷. 이 파일은 이후 불변

raw에 쓰는 스크립트를 만들지 않는 것이 규칙이다. 수집 스크립트조차 이미 있는 raw 파일을 덮어쓰지 말고, 날짜나 버전을 파일 이름에 넣어 새 파일로 남긴다. 원본을 잃으면 그 시점의 데이터는 영영 복원되지 않는다.

스키마와 데이터 계약

원본을 보존하는 것과, 그 원본이 쓸 만한지 아는 것은 다른 문제입니다. 소스는 예고 없이 바뀝니다. 컬럼 이름이 달라지거나, 숫자 컬럼에 문자열이 섞이거나, 음수일 수 없는 값에 음수가 들어옵니다. 이런 변화는 대개 에러를 내지 않고 통과해서, 몇 단계 뒤 모델 성능이 떨어질 때에야 드러납니다. 13편에서 본 데이터 누수처럼, 조용히 지나가는 문제가 가장 비쌉니다.

데이터 계약(data contract)은 데이터가 지켜야 할 약속입니다. 어떤 컬럼이 어떤 타입으로 어떤 범위 안에 있어야 하는지를 명시하고, 수집 시점에 그 약속을 검증합니다. 검증을 파이프라인 입구에 두는 이유는, 문제를 소스와 가장 가까운 곳에서 잡아야 원인을 추적하기 쉽기 때문입니다. 안쪽으로 들어간 뒤에 터지면 어느 단계가 계약을 깼는지 거꾸로 되짚어야 합니다.

pandera로 표 데이터의 계약을 코드로 적을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
import pandera as pa
from pandera import Column, Check

schema = pa.DataFrameSchema({
    "pickup_datetime": Column("datetime64[ns]"),
    "passenger_count": Column(int,   Check.in_range(1, 6)),      # 1명 이상 6명 이하
    "trip_distance":   Column(float, Check.greater_than(0)),      # 이동 거리는 양수
    "fare_amount":     Column(float, Check.greater_than_or_equal_to(0)),  # 요금은 0 이상
})

schema.validate(df, lazy=True)   # 규칙을 어긴 행을 모두 모아 SchemaError로 알린다

lazy=True는 첫 위반에서 멈추지 않고 모든 위반을 한 번에 보고합니다. 어떤 컬럼의 몇 개 행이 범위를 벗어났는지 한눈에 보이므로, 소스의 어디가 깨졌는지 바로 알 수 있습니다. 검증을 통과한 데이터만 다음 단계로 넘기면, 이후 코드는 “값이 계약을 지킨다”를 전제로 깔고 갈 수 있습니다.

검증 규칙은 “이 값이 물리적으로, 사업적으로 가능한가”에서 나온다. 승객 수가 0이거나 이동 거리가 음수인 행은 소스의 오류다. 수집 경계에서 이런 행을 걸러내거나 최소한 기록해두지 않으면, 그 오류가 그대로 특성과 모델로 흘러든다.

왜 데이터에 버전이 필요한가

Git으로 코드는 버전관리가 되지만 데이터는 잘 되지 않습니다. 이유는 두 가지입니다. 첫째, 데이터 파일은 수백 MB에서 수십 GB로 커서 git 저장소에 넣으면 저장소가 감당하지 못합니다. 둘째, git은 텍스트의 줄 단위 변화를 추적하도록 설계돼서, Parquet 같은 이진 파일에는 의미 있는 diff를 만들지 못합니다.

그렇다고 데이터를 버전 없이 두면 재현이 깨집니다. 실험 A는 1월 데이터로, 실험 B는 1월에 2월이 더해진 데이터로 학습했는데 파일 이름이 같다면, 두 실험의 차이가 코드 때문인지 데이터 때문인지 구분할 수 없습니다. 실험 추적(6편)이 코드와 하이퍼파라미터를 기록해도, 데이터가 어느 버전이었는지 함께 남지 않으면 그 기록은 반쪽입니다.

필요한 것은 코드 저장소는 가볍게 두면서, 각 커밋이 “그때 어떤 데이터였는지”를 정확히 가리키게 하는 장치입니다. DVC(Data Version Control)가 그 역할을 합니다.

DVC의 개념과 최소 예

DVC의 아이디어는 단순합니다. 큰 데이터 파일 자체는 git에 넣지 않고, 그 파일의 해시가 담긴 작은 텍스트 파일(.dvc)만 git에 넣습니다. 실제 데이터 본체는 별도의 원격 스토리지(S3, GCS, 사내 서버 등)에 올립니다. git 커밋은 데이터를 가리키는 포인터만 들고 있고, 그 포인터를 따라가면 정확히 그 버전의 데이터를 받을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
dvc init                 # 프로젝트에 .dvc/를 만든다. git에 함께 커밋한다

dvc add data/raw/trips_2025-01.parquet
# 데이터 본체 대신 trips_2025-01.parquet.dvc(해시가 든 텍스트)와
# .gitignore가 생긴다. git이 추적하는 것은 이 .dvc 파일이다

git add data/raw/trips_2025-01.parquet.dvc data/raw/.gitignore
git commit -m "Track January raw trips with DVC"

dvc remote add -d storage s3://my-bucket/dvc   # 데이터 본체가 살 원격 스토리지
dvc push                 # 파일 본체를 원격으로 올린다. git 저장소는 가볍게 유지된다

이렇게 해두면 코드와 데이터가 한 커밋으로 묶입니다. 몇 달 뒤 그 시점의 모델을 재현하려면 두 줄이면 됩니다.

1
2
git checkout <그때의 커밋>   # 코드와 .dvc 포인터가 그 시점으로 돌아간다
dvc pull                     # 포인터가 가리키는 정확한 데이터 버전을 내려받는다

git checkout이 코드를, dvc pull이 데이터를 되돌려, 2편에서 말한 재현성이 데이터까지 확장됩니다. DVC 말고도 lakeFS나 Git LFS 같은 대안이 있지만 해결하려는 문제는 같습니다. 큰 데이터를 git 밖에 두되 git 커밋과 정확히 연결하는 것입니다.

DVC를 더 확장하면 dvc.yaml로 수집, 전처리, 학습 같은 단계의 의존성과 산출물을 선언해, 입력이 바뀐 단계만 다시 실행할 수도 있다. 이 편에서는 데이터 버전관리라는 핵심만 다루고, 파이프라인 선언은 5편의 피처 파이프라인에서 다시 이어간다.

NYC 택시로 보는 실제 수집

일반 원칙을 실제 데이터에 적용한 예가 NYC 택시 파이프라인입니다. 데이터 수집과 EDA 편에서는 뉴욕시 TLC가 공개하는 공식 소스에서 월별 Parquet을 직접 받아 raw로 저장하고, 그 위에서 스키마와 값 범위를 확인합니다. 승객 수, 이동 거리, 요금처럼 이 편에서 계약으로 명시한 컬럼들이 실제로 어떻게 분포하고 어디가 깨져 있는지를 그 글에서 데이터로 확인할 수 있습니다.

정리

개념한 줄 요약
원본 보존소스에서 받은 raw는 불변으로 둔다. 파이프라인은 raw를 읽기만 하고 덮어쓰지 않는다
데이터 소스파일, DB, API 무엇이든 받은 그대로 스냅샷으로 저장하는 것에서 시작한다
데이터 계약컬럼, 타입, 범위를 명시하고 수집 시점에 검증해 문제를 소스 가까이에서 잡는다
pandera표 데이터의 계약을 코드로 적고 lazy 검증으로 위반 행을 한 번에 보고한다
데이터 버전관리모델 재현에는 코드 버전과 데이터 버전이 둘 다 필요하다
DVC데이터 본체는 원격에, 해시 포인터(.dvc)만 git에. 커밋이 데이터 버전을 가리킨다
재현git checkout으로 코드를, dvc pull로 데이터를 그 시점으로 되돌린다

데이터를 원본으로 보존하고 계약으로 검증하고 버전으로 묶었으면, 이제 그 데이터가 실제로 어떤 모습인지 들여다볼 차례입니다. 다음 편은 EDA와 데이터 품질을 자동으로 점검하는 방법입니다.

다음 글: 머신러닝 실전 워크플로 (4) - EDA와 데이터 품질 자동 점검

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.