본문으로 건너뛰기

Drift Monitoring — M1 (read) + M2 (mutation)

Theme G (#1154) → G-1 GAP #1213 의 backend 골격. 가스공사 RFP SFR-009 "정확도·안전성 + 시계열 알림" 대응.

Scope

영역M1 PoC (#1217)M2 (본 PR)M3 / 후속
드리프트 엔진SciPy KS test (수치형)(동일 — service 변경 없음)Evidently AI (PSI + KS + chi-square)
DB 테이블drift_report(동일 — Alembic 없음, baseline 정책)deleted_at 컬럼 + override audit 테이블
APIGET list + GET detailPOST /compute + PUT + DELETEtimeseries + per-feature
UI없음없음/admin/drift 대시보드 + iframe
Slack 알림없음없음 (notified_at 컬럼은 미사용)breach 시 발송 + notified_at 갱신
Batch 통합없음없음ml/batch_predict_asset.py drift_check=True
MLflow REST + Iceberg fetch없음stub (empty df → ok row)실 데이터 fetch

M2 에 포함되지 않은 것 (M3 작업)

  • ❌ Evidently AI 의존성 — 이미지 사이즈 부담 + wire 안정화 우선
  • ❌ MLflow REST 의 reference run artifact 다운로드 + Iceberg current window query — 본 PR 의 compute_and_save 는 빈 DataFrame stub 사용
  • ❌ Slack / Email 알림 발송 + notified_at 갱신
  • ❌ UI 컴포넌트 (/admin/drift 페이지, 시계열 차트, iframe)
  • ❌ Dagster batch asset 통합 (Epic #1083 M2 와 묶음)
  • ❌ Per-feature 시계열 (GET /drift/timeseries)
  • deleted_at 기반 진짜 soft delete tombstone (M2 = hard delete)

구성 요소

1. ORM 테이블 — drift_report

-- apps/api/src/gend_api/db/models/drift.py (ADR-002 baseline, no Alembic)
CREATE TABLE drift_report (
id UUID PRIMARY KEY,
workspace_id UUID NULL REFERENCES workspaces(id) ON DELETE RESTRICT,
ml_batch_run_id UUID NULL REFERENCES ml_batch_run(id) ON DELETE SET NULL,
manual_trigger BOOLEAN NOT NULL DEFAULT FALSE,
model_name VARCHAR(255) NOT NULL,
model_version VARCHAR(32),
reference_run_id VARCHAR(64),
current_data_window_start TIMESTAMPTZ,
current_data_window_end TIMESTAMPTZ,
rows_evaluated BIGINT,
drift_score DOUBLE PRECISION NOT NULL,
drift_status VARCHAR(32) NOT NULL CHECK (drift_status IN ('ok','warn','breach')),
drifted_features JSONB NOT NULL DEFAULT '{}',
notified_at TIMESTAMPTZ,
artifact_uri TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_drift_report_model ON drift_report(model_name, created_at);

workspace_idNULLABLE — Epic #1018 M1 backfill 단계 (ml_batch 패턴 동일). 후속 M3 PR 에서 NOT NULL 전환.

2. 드리프트 계산 — services/drift_service.py

from gend_api.services.drift_service import DriftService

service = DriftService()
report = service.compute_drift(
reference=training_df,
current=batch_df,
threshold=0.3, # 또는 MLBatchJob.drift_threshold 에서 read
)
# report.drift_score: float in [0, 1]
# report.drift_status: 'ok' / 'warn' / 'breach'
# report.drifted_features: {col: p_value, ...}

알고리즘 (PoC):

  1. 두 DataFrame 의 수치형 컬럼 교집합 추출 (select_dtypes(include='number')).
  2. 각 컬럼에 대해 scipy.stats.ks_2samp 호출 → p-value.
  3. p < 0.05 인 컬럼을 drifted_features 맵에 기록.
  4. drift_score = flagged_count / comparable_count (comparable=0 시 0.0).
  5. drift_status:
    • score == 0ok
    • 0 < score ≤ thresholdwarn
    • score > thresholdbreach

수치형이 아닌 컬럼은 silent skip — chi-square 는 M2.

3. API

# Read (M1)
GET /api/v1/models/{model_name}/drift # 리스트 (workspace fence + pagination)
GET /api/v1/models/{model_name}/drift/{report_id} # 단건 (model_name 불일치 → 404)

# Mutation (M2)
POST /api/v1/models/{model_name}/drift/compute # 수동 trigger (viewer+)
PUT /api/v1/models/{model_name}/drift/{report_id} # status override (admin only)
DELETE /api/v1/models/{model_name}/drift/{report_id} # soft delete (admin only, 409 if notified)

리스트 응답 shape — apps/api/src/gend_api/models/drift.py DriftReportListResponse (items: list[DriftReportRead] + total: int):

{
"items": [
{
"id": "uuid",
"workspace_id": "uuid",
"ml_batch_run_id": null,
"manual_trigger": true,
"model_name": "credit_risk",
"model_version": "1",
"reference_run_id": "ref-run-abc",
"drift_score": 0.5,
"drift_status": "breach",
"drifted_features": {"age": 0.001, "income": 0.02},
"notified_at": null,
"artifact_uri": null,
"created_at": "2026-05-28T00:00:00Z"
}
],
"total": 1
}

권한: GET 은 require_viewer (admin / analyst / viewer). Workspace fence 는 routers/ml_batch.py 패턴과 동일 — caller-tenant 행 + legacy NULL 행만 가시.

4. Mutation API (M2)

POST /api/v1/models/{name}/drift/compute — 수동 trigger

권한: require_viewer (모든 인증 사용자). 결과 행은 caller 의 workspace 로 fence.

curl -X POST "https://gend.genon.ai/api/v1/models/credit_risk/drift/compute" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"reference_run_id": "ref-run-abc",
"current_data_window_start": "2026-05-01T00:00:00Z",
"current_data_window_end": "2026-05-08T00:00:00Z",
"threshold": 0.3,
"model_version": "3"
}'
# 201 Created → DriftReportRead body

응답 body 는 GET 단건과 동일한 DriftReportRead shape. manual_trigger=true 가 자동 set 되며, ml_batch_run_id=null (parent batch 없음).

M3 swap 진행 (PR #1471)
  • pipelines 측 (Dagster ml_batch_predict_asset): drift_check=True 인 ml_batch_job 실행 시 pipelines/gend_pipelines/ml/drift.py:compute_ks_drift 로 실 데이터 KS 계산 + drift_report row INSERT. baseline 은 현 batch 의 50% split (앞쪽 reference / 뒤쪽 current) — PoC. 다음 follow-up 에서 MLflow training run artifact 의 dataframe 으로 reference 교체 예정.
  • apps/api 측 (/drift/compute router): M2 PoC 의 빈 DataFrame stub 그대로. 별도 PR 에서 service.compute_and_save 의 fetch 로직 도입.
  • cross-PR 정합: tests/test_drift_module.py::test_default_drift_threshold_matches_apps_api 가 양쪽 DEFAULT_DRIFT_THRESHOLD literal 정합 강제.

검증 (Pydantic):

  • reference_run_id — required, 1–64 자
  • current_data_window_start/end — required ISO 8601 datetime
  • threshold[0.0, 1.0] 범위 (out → 422)
  • model_version — optional, max 32 자

PUT /api/v1/models/{name}/drift/{report_id} — admin status override

권한: require_admin (admin only). False-positive 표시 등 수동 재분류.

curl -X PUT "https://gend.genon.ai/api/v1/models/credit_risk/drift/${REPORT_ID}" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"drift_status": "warn",
"override_reason": "upstream data quality blip — verified false positive"
}'
# 200 OK → DriftReportRead body (updated drift_status)

정책:

  • drift_status 는 enum ok | warn | breach 만 허용 (다른 값 → 422)
  • override_reason필수 (3–500 자) — 빈 override 를 막아 paper trail 강제
  • override 액션은 gend.audit 로거에 drift_status_override 구조화 라인 + AuditMiddleware 의 JSON request-body capture 양쪽에 기록 (M3 OpenSearch ingestion 까지 보존)
  • Workspace fence: admin 은 모든 워크스페이스 행 override 가능 (ml_batch 와 동일 정책). M3 에서 strict tenant 분리 검토.
  • 본 PR 에서 override_reason 컬럼은 추가하지 않음 (Alembic baseline 정책) — audit 로그가 단일 진실 공급원.

DELETE /api/v1/models/{name}/drift/{report_id} — soft delete

권한: require_admin. 알림이 발송된 행은 보존.

curl -X DELETE "https://gend.genon.ai/api/v1/models/credit_risk/drift/${REPORT_ID}" \
-H "Authorization: Bearer ${ADMIN_TOKEN}"
# 204 No Content (성공)
# 404 — 행이 없거나 model_name 불일치
# 409 — notified_at != NULL (M3 알림 발송 후) → "Cannot delete a drift report that has already triggered a notification"

정책:

  • M2 에서는 hard DELETE (Alembic baseline 정책상 deleted_at 컬럼 추가 불가). M3 에서 tombstone update 로 전환.
  • notified_at IS NOT NULL 인 행은 보존: breach 알림과 근거 데이터 cross-reference 가능해야 한다.
  • 액션은 gend.audit 로거에 drift_report_soft_delete 라인으로 기록.

회귀 가드

  • tests/test_drift_service.py (16 tests) — KS test, 분류기 boundary, threshold validation, M2 create_report / update_status / soft_delete / compute_and_save.
  • tests/test_drift_router.py (31 tests) — 권한 fence, 404/403/422 분기, trailing slash, M2 POST/PUT/DELETE 권한·workspace·enum·409 notified 가드.
  • tests/test_no_drift_threshold_hardcoded.pythreshold=<float> 리터럴 차단 (allowed: drift_service 모듈 상수 + ORM server_default + 테스트 fixture).

M3 마일스톤 — 후속 작업

issue #1213 의 나머지 acceptance criteria:

  1. MLflow REST + Iceberg fetchDriftService.compute_and_save 의 stub 분기를 실 데이터 페치로 swap. 시그니처는 변경 없음 (M2 PR 의 docstring 영역만 교체).
  2. Evidently AI 통합DriftService.compute_drift 의 SciPy 분기를 Evidently Report 호출로 swap. DB / wire shape 동일 → service-layer-only 교체.
  3. Slack 알림services/notification/ provider 사용, breach 시 notified_at 갱신.
  4. UI /admin/driftcomponents/admin/Drift/DriftDashboard.tsx + iframe Evidently HTML.
  5. Batch asset 통합 — Epic #1083 M2 의 ml/batch_predict_asset.py 가 끝나는 시점에 drift_service.compute_and_save() 호출.
  6. GET /drift/timeseries — UI 차트용 score 히스토리.
  7. deleted_at 컬럼 + override audit 테이블 — Alembic migration 동반 PR.

관련

  • 부모 #1154 Theme G — MLOps Governance
  • 인접 #1083 ML Batch Inference (drift_check 활성화 의존)
  • 인접 #1095 ml_batch PR — 본 PoC 의 패턴 원본 (workspace fence, fail-closed, dataclass result)