재현 가능한 데이터 분석 프로젝트 구조

src/content/documents/data-analysis/reproducible-data-analysis-project-structure.json

6개월 전에 짠 분석 노트북을 다시 열었을 때, 어떤 셀을 어떤 순서로 실행해야 하는지 기억나지 않는 경험은 흔하다. 동료가 그 노트북을 받았을 때는 더 막막하다. 재현 가능한 프로젝트 구조는 "6개월 뒤의 나, 또는 처음 보는 동료가 README만 읽고 바로 실행할 수 있는가"를 기준으로 폴더와 코드를 배치하는 방식이다.

한 줄 정의

재현 가능한 프로젝트 구조는 탐색(notebook)과 재사용 코드(src)의 역할을 분리 하고, 데이터·환경·실행 방법을 requirements.txtREADME.md 로 명시해 누구나 같은 결과를 다시 만들 수 있게 하는 폴더 배치다.

Jupyter Notebook vs .py 스크립트 — 언제 무엇을

상황

Jupyter(.ipynb)

Python(.py)

목적

EDA·탐색, 시각화 중심 분석

반복 실행 자동화, 라이브러리 코드

실행 방식

셀 단위로 한 번씩 확인하며 진행

처음부터 끝까지 통째로 실행

버전 관리

diff가 지저분하고 코드 리뷰에 부적합

Git 히스토리·pytest·코드 리뷰 대상

최종 형태

nbconvert로 보고서·HTML 변환

CI/CD 파이프라인에 그대로 통합

둘 중 하나를 골라야 하는 것이 아니다. 탐색은 노트북에서 자유롭게 하고, 검증이 끝난 함수만 .py 모듈로 옮겨 재사용한다. 이 둘을 섞어서 한 파일에 실험 코드와 재사용 코드를 같이 두면, 나중에 어떤 셀이 최종 로직인지 구분할 수 없게 된다.

프로젝트 폴더 구조

data-project/
├── data/
│   ├── raw/            # 원본 데이터 (절대 수정 금지)
│   ├── processed/       # 전처리 완료 데이터
│   └── external/        # 외부 참조 데이터
├── notebooks/           # EDA·실험용 Jupyter 노트북
│   ├── 01_eda.ipynb
│   └── 02_feature_exploration.ipynb
├── src/
│   └── mypkg/            # 재사용 가능한 Python 패키지
│       ├── __init__.py
│       ├── clean.py       # 전처리 함수
│       └── viz.py         # 시각화 함수
├── tests/
│   └── test_clean.py     # src 모듈에 대한 pytest
├── .github/workflows/
│   └── ci.yml            # pytest·ruff 자동 실행
├── output/               # 생성된 리포트·모델 파일
├── .gitignore
├── .env.example
├── pyproject.toml
├── requirements.txt
└── README.md

이 구조에서 성격이 다른 세 가지가 뚜렷하게 나뉜다: 데이터(data/) 는 커밋하지 않는 원자료, 코드(src/, tests/) 는 Git으로 관리하는 재사용 자산, 실험(notebooks/) 은 자유롭게 고쳐도 되는 탐색 공간이다.

모듈화 — 노트북 코드를 재사용 가능한 패키지로

노트북에서 반복해서 쓰는 함수가 보이면 src/ 로 옮긴다. 폴더에 __init__.py 가 있으면 Python이 그 폴더를 패키지로 인식한다.

# src/mypkg/clean.py
def clean_nulls(df, cols=None, strategy="median"):
    cols = cols or df.select_dtypes("number").columns.tolist()
    if strategy == "median":
        return df.fillna(df[cols].median())
    elif strategy == "drop":
        return df.dropna(subset=cols)
    return df

노트북에서 이 함수를 불러오는 방법은 두 가지다. 가장 흔한 방법은 sys.path 에 프로젝트 루트를 직접 추가하는 것이다.

# notebooks/01_eda.ipynb 첫 셀
import sys
sys.path.insert(0, "..")
from src.mypkg.clean import clean_nulls

다만 이 방법은 노트북을 어느 폴더에서 실행하느냐에 따라 경로가 어긋나기 쉽다. 더 안정적인 방법은 pyproject.toml 을 최소한으로 작성하고 패키지를 editable 모드 로 설치하는 것이다 — 한 번 설치해두면 노트북이 어느 위치에 있든, 어디서 실행하든 같은 방식으로 불러온다.

[project]
name = "mypkg"
version = "0.1.0"
requires-python = ">=3.11"

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
pip install -e .
# 이제 어디서든 sys.path 조작 없이 바로 import
from mypkg.clean import clean_nulls
print(clean_nulls)
<function clean_nulls at 0x102229e40>

-e (editable) 옵션은 패키지를 복사하지 않고 현재 소스 위치를 그대로 가리키게 설치한다. src/mypkg/clean.py 를 고치면 재설치 없이 바로 반영된다.

README.md — 실행 방법을 코드로 남긴다

README는 프로젝트 목적을 설명하는 글이 아니라, 그대로 복사해서 실행하면 되는 명령어 모음 에 가까워야 한다.

## 프로젝트 개요
지역별 매출 데이터를 정제·분석하고 예측 모델을 만든다.

## 개발 환경 설정
```bash
git clone <url> && cd data-project
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

실행 방법

pytest tests/
python src/run_pipeline.py

테스트 통과 여부·커버리지를 보여주는 배지(badge)를 README 상단에 붙이면, 코드를 열어보지 않고도 현재 상태를 바로 알 수 있다. 노트북은 jupyter nbconvert --to html notebooks/01_eda.ipynb 로 정적 HTML로 변환해 코드 실행 환경이 없는 사람과도 공유할 수 있다.

핵심 원칙 네 가지

원칙

내용

탐색과 재사용 분리

노트북은 실험용, 검증된 함수만 src/로 옮겨 재사용 가능한 모듈로 관리한다.

재현성 = 신뢰성

requirements.txt·.env.example·README 실행 가이드 이 셋만 있으면 누구나 같은 결과를 재현할 수 있다.

데이터는 Git에 올리지 않는다

data/raw/는 .gitignore에 추가하고, 데이터 출처(URL·수집 스크립트)를 README에 남긴다.

점진적 개선

처음부터 완벽한 구조를 만들 필요는 없다. 분석이 반복되면 함수화하고, 함수가 쌓이면 모듈화한다.

장점과 한계

구분

내용

장점

코드 리뷰·CI·재사용이 가능해지고, 새 팀원이 프로젝트에 합류하는 데 걸리는 시간이 크게 줄어든다.

한계

구조를 갖추는 데 초기 비용이 든다. 한 번 쓰고 버릴 짧은 탐색 작업까지 매번 이 구조를 다 갖출 필요는 없다 — 반복될 가능성이 보일 때 갖춘다.

자주 발생하는 문제

노트북 하나에 실험과 최종 로직이 뒤섞여 있다. 시행착오 셀과 실제로 쓰는 함수가 같은 노트북에 있으면, 다음에 열었을 때 어디까지가 최종본인지 알 수 없다. 검증이 끝난 부분은 바로 src/ 로 옮기고 노트북에서는 그 함수를 import 해서 쓴다.

data/ 폴더를 통째로 커밋한다. 원본 CSV·Parquet 파일은 수십~수백 MB를 넘기기 쉽다. 한 번 커밋된 대용량 파일은 히스토리에서 지워도 저장소 크기가 줄지 않는다. 처음부터 data/.gitignore 에 넣고, 데이터를 받는 방법(다운로드 링크·스크립트)만 README에 남긴다.

# .gitignore
.venv/
.env
__pycache__/
*.pyc
data/raw/
data/processed/
output/

requirements.txt에 버전을 고정하지 않는다. pandas 한 줄만 적어두면, 나중에 새 버전이 설치되면서 동작이 달라질 수 있다(26편에서 본 Pandas 2.x의 Copy-on-Write 변화가 그런 예다). pip freeze > requirements.txt 로 실제 설치된 버전을 그대로 고정해 저장한다.

관련 문서

데이터 파일을 다루는 구체적인 방법은 CSV·JSON·Parquet 파일 읽고 쓰기 를, .env 로 비밀정보를 분리하는 방법은 환경변수와 .env로 비밀정보 관리하기 를 참고한다. 마지막 편에서는 이 구조를 실제 프로젝트 하나에 적용한다.

참고 자료

댓글 0

댓글을 불러오는 중…