- docs/prediction-analysis.md 신설 — opus 4.7 독립 리뷰 기반 prediction 구조/방향 심층 분석 (9개 섹션: 아키텍처·5분 사이클·17 알고리즘·4대 도메인 커버리지·6축 구조 평가·개선 제안 P1~P4·임계값 전수표) - AGENTS.md / README.md — V001~V016→V030, Python 3.9→3.11+, 14→17 알고리즘 모듈 - docs/architecture.md — /gear-collision 라우트 추가 (26→27 보호 경로) - docs/sfr-traceability.md — V029→V030, 48→51 테이블, SFR-10 에 GEAR_IDENTITY_COLLISION 추가 - docs/sfr-user-guide.md — 어구 정체성 충돌 페이지 섹션 신설 - docs/system-flow-guide.md — 노드 수 102→115, V030 manifest 미반영 경고 - backend/README.md — "Phase 2 예정" 상태 → 실제 운영 구성 + PR #79 hotfix 요구사항 전면 재작성
22 KiB
22 KiB
Prediction 모듈 심층 분석 — 구조·방향 리뷰
대상: prediction/ (Python 3.11+, FastAPI, APScheduler, 59 .py 파일)
작성일: 2026-04-17
작성 관점: opus 4.7 독립 리뷰 — 정밀도 튜닝이 아닌 방향성·코드 구조
전제: 프로토타입·데모 단계. 정밀도 미흡은 인지된 상태.
TL;DR — 3줄 요약
- 뼈대는 튼튼하다. 레이어 분리(algorithms/pipeline/output/db/cache), 순수함수 위주 알고리즘, 카테고리별 dedup 윈도우 분리까지 프로토타입치고는 일관된 설계.
- 약한 고리는 오케스트레이터. scheduler.py run_analysis_cycle() 한 함수가 700+ 라인, 지역 try/except +
logger.warning로 흡수된 실패가 많아 "어디서 깨졌는지 조용히 묻힌다". 상태 누적(모듈 전역_transship_pair_history)도 여기 묶여 있음. - 커버리지 매트릭스는 4/4 이지만 UI 비대칭. prediction 이 생산하는 결과 중
ILLEGAL_FISHING_PATTERN이벤트·환적 의심은 DB·백엔드까지 도달하지만 전용 detection UI 가 없다. prediction 품질 개선과 무관하게 운영자가 쓸 수 없는 상태.
권고 최우선 3가지 — 신규 알고리즘 추가보다 아래가 선행:
- P1: 사이클 스테이지 단위 에러 경계(
_stage(...)유틸)로 교체해 실패 스테이지 명시 로깅 + 부분 실패 시에도 후속 단계 진행 - P1: 하드코딩 임계값(MID prefix, 커버리지 박스, SOG band, 11 pattern 점수) 을
correlation_param_models패턴처럼 DB/config 로 외부화 - P1: 환적 전용 + ILLEGAL_FISHING_PATTERN 전용 프론트 페이지 추가 — 이미 DB·API 는 있음
1. 아키텍처 레이어 — 책임과 결합도
prediction/
├── config.py Pydantic Settings + qualified_table() — SSOT 설정
├── scheduler.py APScheduler + run_analysis_cycle() (단일 엔트리)
├── main.py FastAPI app + /health /status /chat 등
├── fleet_tracker.py 상태 보유 (선단 레지스트리, 어구 정체성)
├── time_bucket.py 안전 지연 12분 + backfill 3 버킷
├── algorithms/ 17개 모듈 — 순수함수 중심
├── pipeline/ 8개 모듈 — 7단계 분류 파이프라인
├── output/ 5개 모듈 — event/violation/kpi/stats/alert
├── db/ 4개 모듈 — snpdb / kcgdb / signal_api / partition_manager
├── cache/ vessel_store.py — 24h sliding window 인메모리
├── chat/ Ollama + RAG 스텁
├── models/ result.py — AnalysisResult dataclass
├── data/ monitoring zones JSON 정적 설정
└── tests/ time_bucket / gear_parent_episode / gear_parent_inference 3종
| 레이어 | 책임 | 결합도 평가 |
|---|---|---|
config.py |
환경 + SQL identifier 검증 | ✅ SSOT, qualified_table() 로 스키마 주입 공격 방지 |
algorithms/ |
탐지 로직 (순수) | ✅ 대부분 df, params -> dict/tuple. 상호 의존 적음 |
pipeline/ |
7단계 sequential | ✅ orchestrator.ChineseFishingVesselPipeline.run() 이 DF 를 그대로 파이프 |
output/ |
룰 엔진 + DB write | ✅ 룰을 lambda 리스트(event_generator.RULES)로 선언적 관리 |
db/ |
Connection pool + SQL | ⚠️ kcgdb.upsert_results(results) 가 한 트랜잭션에 전부 묶임 (파티션 unique index 활용은 적절) |
cache/vessel_store.py |
전역 싱글턴, 24h 궤적 인메모리 | ⚠️ 모듈 싱글턴 → 테스트 시 mock 어려움 |
fleet_tracker.py |
레지스트리·어구 정체성 상태 | ⚠️ 싱글턴 + 모듈 전역 캐시 |
scheduler.py |
전체 오케스트레이션 | ❌ 700+ 라인 모놀리식 — 아래 §2 상세 |
2. 5분 사이클 시퀀스 — scheduler.py:80-804
사이클 전체가 한 함수 안에서 9단계로 진행된다.
| 단계 | 라인 | 역할 | 실패 처리 |
|---|---|---|---|
| 1. 증분 로드 | 97-106 | snpdb.fetch_incremental() → vessel_store merge |
전체 try/except 포함 |
| 2. 정적 보강 | 108-112 | signal-batch API 호출 | 전체 try/except |
| 3. 대상 선별 | 114-128 | SOG/COG 계산 + 0건 시 조기 return | ✅ 조기 반환 |
| 4. 파이프라인 | 122-128 | ChineseFishingVesselPipeline.run() |
전체 try/except |
| 5. 선단 분석 | 131-177 | fleet_tracker + gear_identity 충돌 감지 | ⚠️ 내부 try/except 로 warning, 전진 |
| 5.5. 어구 그룹·상관·부모 추론 | 181-229 | polygon_builder + gear_correlation | ⚠️ 내부 try/except, 결과 없이 진행 |
| 5.9. 쌍끌이 후보·판정 | 231-303 | pair_trawl STRONG/PROBABLE/SUSPECT | ⚠️ 내부 try/except |
| 6. 개별 선박 분석 | 305-515 | AnalysisResult 생성 (파이프라인 통과자) | 루프 내 continue |
| 6.5. 경량 분석 | 523-682 | 파이프라인 미통과 중국 MID — compute_lightweight_risk_score |
루프 내 try/except |
| 7. 환적 의심 | 685-713 | detect_transshipment + _transship_pair_history 누적 |
전체 try/except |
| 8. DB upsert | 716-717 | kcgdb.upsert_results() + cleanup_old(48h) |
전체 try/except |
| 9. 출력 모듈 | 720-745 | violation_classifier → event_generator → kpi_writer → aggregate_hourly/daily → alert_dispatcher | ⚠️ 5개 단계를 한 try/except 로 묶음 → 어디서 실패했는지 단일 warning 으로 흡수 |
| 10. 채팅 컨텍스트 캐싱 | 748-791 | Redis | 전체 try/except |
구조적 관찰
- 전체 try/except 는 있다 (97 ~ 803) — 치명 실패가 다음 사이클을 막지는 않음
- 그러나 내부 스테이지가 너무 무겁다. 9번째 출력 단계가 5개 모듈을 한 덩어리로 묶어
logger.warning('output modules failed (non-fatal): %s', e)로 흡수. 어느 모듈이 깨졌는지 디버깅하려면 stacktrace 를logger.exception으로 바꿔야 함 - Lazy import 가 스테이지마다 반복 (
from output.event_generator import ...등). 시작 시간 단축에는 도움이지만 import 오류가 첫 사이클 실행 시점에만 드러남 — 배포 후 5분 지연 발견 경험 다수
권고 (사이클 구조 재정비)
def _stage(name: str, fn, *args, required=False, **kwargs):
t0 = time.time()
try:
result = fn(*args, **kwargs)
logger.info('stage %s ok in %.2fs', name, time.time() - t0)
return result
except Exception as e:
logger.exception('stage %s failed: %s', name, e)
if required:
raise
return None
- 각 스테이지를
_stage('pair_detection', _run_pair_detection, ...)로 감싸면 실패 스테이지 명시 로깅 + stacktrace + 부분 실패 허용 정책을 일관화. - 9번 단계의 5개 모듈은 각각 별도
_stage(...)호출로 쪼갤 것.
3. 알고리즘 카탈로그 (17 모듈 × 담당 도메인)
| 파일 | 주 역할 | 입력 | 출력 | 주요 상수 |
|---|---|---|---|---|
| dark_vessel.py | AIS gap + 11 패턴 점수화 | 선박 DF(timestamp, lat, lon, sog, cog) | (score 0~100, patterns[], tier) | GAP_SUSPICIOUS=6000s, VIOLATION=86400s, KR_COVERAGE box |
| spoofing.py | 텔레포트·극속·BD09 오프셋 | 선박 DF | spoofing_score 0~1 | EXTREME_SPEED=50kn (주석 기준), fishing max=25kn |
| risk.py | 종합 risk + 경량 risk 2종 | DF, zone, is_permitted, 외부 점수 | (risk 0~100, level) | tier: 70+=CRITICAL, 50+=HIGH, 30+=MEDIUM |
| fishing_pattern.py | UCAF/UCFT gear SOG band | DF, gear | (ucaf, ucft) | PT 2.5~4.5, OT 2.0~4.0, GN 0.5~2.5 |
| transshipment.py | 5단계 필터 파이프라인 | DF targets, pair_history, zone_fn | list of dict | PROXIMITY ~220m, RENDEZVOUS 90min, WATCH 제외 |
| location.py | zone 분류, haversine_nm, BD09 | (lat, lon) | zone, dist_nm | 12/24 NM 밴드 |
| gear_correlation.py | 멀티모델 EMA + streak | vessel_store, gear_groups, conn | UPDATE gear_correlation_scores | α_base=0.30, polygon=0.70 |
| gear_identity.py | 공존 쌍 추출 (V030/PR #73) | gear_signals | collisions[] | CRITICAL_DIST=50km, COEXIST=3회 |
| gear_parent_inference.py | 어구 → 모선 assignment | gear_groups + tracks | parent 후보 + confidence | 2-pass: direct-match → candidates |
| gear_parent_episode.py | 에피소드 delineation (first_seen~last_seen) | gear_signals 시계열 | episodes[] | gap tolerance |
| gear_violation.py | G-01~G-06 통합 판정 (DAR-03) | DF + zone + pair_result + permits | g_codes[], evidence, score | G-06=20pts, G-02=18pts, G-01=15pts |
| gear_name_rules.py | 어구 이름 정규표현식 | string | parent_code (Optional) | regex set |
| pair_trawl.py | 쌍끌이 tier 분류 | DF×2, 6h | (pair_detected, tier, pair_type) | PROXIMITY=0.27NM, MIN_SYNC=2h |
| track_similarity.py | DTW 궤적 유사도 | DF×2 | 0~1 | - |
| fleet.py | leader/follower/independent | DF, tracker | role | - |
| polygon_builder.py | gear group convex hull | vessel_store, companies | 스냅샷[] | 시간버킷 |
| vessel_type_mapping.py | fishery_code → vessel_type 폴백 | fishery_code | 'TRAWL'/'PT'/... | - |
관찰:
- 대부분 순수 함수 → 재사용·단위테스트 용이. 단
gear_correlation.run_gear_correlation은conn을 받아 DB 를 직접 UPDATE 함 (알고리즘 + I/O 혼재) - 상수가 모듈 상단에 모여 있는 것은 좋으나 config 외부화는 안 됨. 현장 운영자가 임계값을 바꾸려면 코드 배포 필요
4. 4대 도메인 커버리지 매트릭스
| 도메인 | 담당 파일 | 접근 방식 | 구조적 강점 | 구조적 공백 |
|---|---|---|---|---|
| Dark Vessel | dark_vessel.py + risk.py + compute_dark_suspicion |
gap(100분 임계) + 11 패턴 점수 + 4-tier (CRITICAL≥70) | 패턴 분리(P1~P11) + 한국 커버리지 감점(−50pt)으로 자연 gap 구분 | MID/커버리지 박스 하드코딩. 감점 로직은 탐지 누락 위험(coverage box 바깥=자연 gap 가정) — 중국 EEZ 해역 근접 공해에서 의도적 OFF 가 감점 맞을 수 있음 |
| Gear Tracking | gear_correlation + gear_identity + gear_parent_inference + polygon_builder |
어구 이름 regex → 공존/교체 분기 + 멀티모델 EMA + 모선 추론 + 폴리곤 | 공존(GEAR_IDENTITY_COLLISION)을 1순위 증거로 재정의 (V030). SAVEPOINT 로 PK 충돌 격리 | 어구 신호는 이름 regex 의존. 이름이 GEAR_PATTERN 에 맞지 않으면 파이프라인 전체 진입 실패 |
| Chinese Fishing | pipeline/ 7단계 + MID 412/413/414 필터 |
preprocess → behavior → resample → feature → classify → cluster → seasonal + rule-based 분류 | 룰 + 군집으로 해석 가능 | MID prefix 하드코딩 2곳 중복. 전체 인구(55k)대비 500척만 파이프라인 통과 — 경량 분석(§6)으로 보완하나 여전히 <10% |
| Illegal Pattern | gear_violation (G-01~G-06) + event_generator.RULES (15+ 룰) + transshipment 5단계 |
임계값 기반 독립 룰 × 카테고리별 dedup 윈도우 | 룰이 lambda 리스트로 선언적 | UI 미노출 — DB prediction_events 카테고리 ILLEGAL_* 는 생산되나 전용 detection UI 없음. 운영자는 EventList(/event-list) 에서만 조회. 환적도 동일 문제 |
5. 코드 구조 평가 (6축)
| 축 | 평가 | 근거 |
|---|---|---|
| 관심사 분리 | B+ | algorithms / pipeline / output / db 레이어는 깔끔. 단 scheduler 는 오케스트레이터가 아니라 메가 함수 |
| 재사용성 | A- | 17 알고리즘 모듈 중 ~15개가 순수함수. run_gear_correlation 만 conn 혼재 |
| 테스트 가능성 | C+ | unit test 3개만 (time_bucket / gear_parent_episode / gear_parent_inference). vessel_store / fleet_tracker 싱글턴 → integration test 어려움 |
| 에러 격리 | C | 사이클 전체 try/except + 내부 지역 try/except 혼재. 출력 5모듈이 한 덩어리 → 실패 지점 특정 불가 |
| 동시성 | A- | ThreadedConnectionPool(1,5), max_instances=1 스케줄러 — 단일 프로세스 가정 하에서 안전 |
| 설정 가능성 | C- | 임계값 대부분 파일 상수. correlation_param_models 패턴만 DB 기반 (예외) |
주목할 잘된 점
- Dedup 윈도우 카테고리별 차등 (event_generator.py:26-39) — 5분 boundary 집단 만료를 피하기 위해 33/67/89/127/131/181/367분 등 의도적으로 5의 배수 회피. 룰 기반 탐지의 대표적인 "튜닝 knob" 이 코드에 명시.
- gear_identity 공존/교체 분기 (fleet_tracker.py 트랜잭션 설계) — SAVEPOINT 로 부분 실패를 사이클 전체 abort 와 분리. 이전 13h 공백 사고의 재발 방지 설계가 구조에 반영됨.
- Lightweight path — 파이프라인 통과 못한 중국 MID 를 경량 경로로 계속 커버 (scheduler.py:523-682). "정밀 vs 커버리지" 를 두 경로로 나누는 의사결정 자체는 탁월.
구조적 채무
- 환경 분기 부재:
config.py에 dev/prod 분기가 없음..env파일 하나에 의존. 로컬 실행 시 운영 DB 를 건드리는 위험 (config.py:8-22) - 상태 있는 모듈 전역 변수:
_transship_pair_history,_last_run,_scheduler(scheduler.py:16-26). 테스트 격리 어렵고, 재시작 시 pair 누적 상태 증발 - DB 쓰기 산재:
kcgdb.upsert_results/save_group_snapshots/gear_correlation UPDATE/gear_identity UPSERT/prediction_events INSERT가 서로 다른 트랜잭션. 한 사이클 원자성 X — 의도적일 수 있으나 명시 설계 문서 없음
6. 방향성 진단 — 프로토타입 → MVP → 운영
지금 강점
- 룰 기반 탐지를 탄탄히 다져둔 토대 — 임계값이 드러나 있고 dedup 설계가 명시적. 향후 ML overlay 를 얹을 때 "어디에 얹을지" 가 명확 (dark_suspicion score, transship score, gear_violation score 가 모두 연속값으로 산출)
- 운영자 의사결정 통합 설계 — V030 GEAR_IDENTITY_COLLISION 에서 status(OPEN/REVIEWED/CONFIRMED_ILLEGAL/FALSE_POSITIVE) 가 severity 재계산을 억제하는 패턴 — 사람 loop back 이 설계된 유일한 자리. 다른 도메인에도 이 패턴 확장 가능
지금 약점
- ML 부재 — sklearn/torch 없음. 2026 기준 프로토타입으로는 적절하나, sequence anomaly (dark gap 의 시계열 반복 패턴) 나 behavioral classifier 는 룰만으론 한계
- 하드코딩 지대: MID prefix(4개소), KR coverage box, SOG band, 11 패턴 점수, 5단계 transship 임계 — 모두 "이 프로젝트에서 튜닝해야 할 핫스팟" 인데 DB/config 분리 안 됨
- UI 비대칭:
prediction_events.category='ILLEGAL_FISHING_PATTERN'이 생산되지만 전용 UI 없음. 환적도 동일. 결과적으로 prediction 이 만드는 가치의 일부가 운영자에게 도달하지 못함 - 테스트 빈곤: 17 알고리즘 중 3개만 유닛테스트. 사이클 단위 integration test 전혀 없음 — 사이클 회귀가 항상 운영 로그로만 드러남 (13h 공백 사고가 대표 사례)
7. 구조적 개선 제안 (우선순위별)
P1 — 지금 해야 할 것 (운영 안정성)
- 사이클 스테이지 단위 에러 경계 —
_stage(name, fn, required=False)유틸로 9번 출력 5모듈을 쪼갤 것.logger.exception으로 stacktrace 보존.required=True를fetch_incremental같은 fatal 에만 적용 → 실패 시 조기 반환 - 임계값 외부화 —
correlation_param_models패턴을 확장해detection_params테이블 신설 (algo_name, param_key, value, active_from, active_to). 배포 없이 해상도 튜닝 가능. 운영자 권한으로 접근 시 감사 로그 - ILLEGAL_FISHING_PATTERN 전용 페이지 + 환적 전용 페이지 — 백엔드 API·DB 는 이미 존재. 프론트만 GearCollisionDetection 패턴으로 추가 (
PageContainer+DataTable+Badge intent) - 사이클 부분 원자성 명시 — DB 쓰기 경계 문서화 (어디까지가 한 트랜잭션인지). 최소한 architecture.md 또는 신설
docs/prediction-transactions.md에 다이어그램
P2 — 다음 (품질 확보)
- 알고리즘 유닛테스트 커버리지 — 17 모듈 중 최소 10 개 (dark_vessel 11 패턴 / transshipment 5단계 / gear_violation 6 G-code / spoofing / risk) 에 fixture 기반 테스트.
tests/fixtures/에 AIS DF CSV 샘플 - DB fixture integration test — testcontainers-python 으로 PostgreSQL 띄워 한 사이클 실행 + 결과 테이블 assert. CI 에서 돌릴 수 있도록 데이터 10 척 x 1h 정도 경량
vessel_store/fleet_tracker의존성 주입 개편 — 모듈 싱글턴 →AnalysisContextdataclass 로 명시 주입. 테스트 mock 가능- MID prefix·커버리지 box 를
monitoring_zonesJSON 연장 — 이미data/monitoring_zones.json이 있음. 동일 포맷으로mid_prefixes.json/kr_coverage.json추가
P3 — 중기 (가치 확장)
- ML overlay 타겟 설정 — dark_suspicion score (11 패턴 합산) 은 classifier training target 으로 최적. GCN/Transformer 로 "gap 시퀀스가 의도적인가" 를 학습. 룰 유지 + 게이트만 ML 로 대체 (shadow mode 로 비교)
- correlation 파라미터 MLOps 연동 —
correlation_param_models를 MLflow 로 실험 기록 → 성능 좋은 모델 자동 active 전환 - AIS 벤치마크 데이터셋 — 한중어업협정 906척 중 과거 단속 이력 있는 선박을 positive label. 현재 매칭률 53%+ 이므로 샘플 확보 가능. tier 별 precision/recall 산출
P4 — 장기 (스케일)
- multi-process / async — APScheduler + 단일 스레드 한계. 현 55k 선박 / 2.3M points / 110초 사이클에서 8k 중국 증가 + 한국 확장 시 5분 주기 내 완료 불가 예측. asyncio + ray / dask 로 스테이지 병렬
- Event bus 분리 — 지금은
prediction_eventsINSERT 가 동기. outbox 패턴으로 비동기 분리 시 백엔드/프론트 실시간 push 기반 (WebSocket) 로 진화 가능
8. 부록 — 임계값 전수표 (외부화 우선순위)
| 위치 | 상수 | 현재값 | P1 외부화 | 비고 |
|---|---|---|---|---|
| scheduler.py:28 | _KR_DOMESTIC_PREFIXES |
('440','441') |
✅ | 한국 MID |
| scheduler.py:140,247 | 중국 MID prefix | '412' '413' '414' 하드코딩 2곳 |
✅ | mid_prefixes.json |
| scheduler.py:256 | pair pool SOG band | 1.5 <= mean_sog <= 5.0 |
✅ | 조업 속력대 |
| dark_vessel.py:6-8 | GAP 임계 3종 | 6000/10800/86400s | ✅ | 100분/3h/24h |
| dark_vessel.py:11-12 | _KR_COVERAGE_LAT/LON |
32.0~39.5 / 124.0~132.0 | ✅ | AIS 수신 박스 |
| dark_vessel.py:257-359 | 11 패턴 점수 P1~P11 | 10~30 pt 분산 | ⚠️ | 탐지 정책 튜닝 대상 |
| event_generator.py:26-39 | DEDUP_WINDOWS 12 카테고리 | 33~367분 | ⚠️ | 이미 의도적 |
| config.py:25-37 | 파이프라인 주기 등 | 5/24/60/30/12/3 | ✅ env | .env 로 이미 가능 |
| transshipment.py | PROXIMITY / RENDEZVOUS | 0.002deg / 90min | ✅ | 환적 민감도 |
| pair_trawl.py | PROXIMITY / SOG_Δ / COG_Δ / MIN_SYNC | 0.27NM / 0.5kn / 10° / 2h | ⚠️ | tier 재분류 기준 |
P1: 배포 없이 튜닝 가능해야 할 것 ⚠️: 튜닝 자체가 탐지 정책 변경 → 릴리즈 노트 필요
9. 관련 파일 인덱스
- 오케스트레이터: scheduler.py
- 설정: config.py
- 상태 컴포넌트: fleet_tracker.py, cache/vessel_store.py
- 알고리즘: algorithms/
- 파이프라인: pipeline/orchestrator.py
- 출력: output/event_generator.py, output/violation_classifier.py, output/kpi_writer.py, output/stats_aggregator.py, output/alert_dispatcher.py
- DB: db/kcgdb.py, db/snpdb.py, db/partition_manager.py
연관 운영 문서:
- architecture.md — 프론트엔드 아키텍처
- sfr-traceability.md — SFR 매트릭스
- system-flow-guide.md — system-flow 노드 명세
backend/README.md— 백엔드 구성 (V030 + PR #79 hotfix 요구사항 명시)
변경 이력
| 일자 | 내용 |
|---|---|
| 2026-04-17 | 초판 — opus 4.7 독립 리뷰. 구조/방향 중심 + 우선순위별 개선 제안 |