kcg-monitoring/docs/GEAR-PARENT-INFERENCE-ALGORITHM-SPEC.md
htlee 7dd46f2078 feat: 어구 모선 추론(Gear Parent Inference) 시스템 이식
Codex Lab 환경(iran-airstrike-replay-codex)에서 검증 완료된
어구 모선 자동 추론 + 검토 워크플로우 전체를 이식.

## Python (prediction/)
- gear_parent_inference(1,428줄): 다층 점수 모델 (correlation + name + track + prior bonus)
- gear_parent_episode(631줄): Episode 연속성 (Jaccard + 공간거리)
- gear_name_rules: 모선 이름 정규화 + 4자 미만 필터
- scheduler: 추론 호출 단계 추가 (4.8)
- fleet_tracker/kcgdb: SQL qualified_table() 동적화
- gear_correlation: timestamp 필드 추가

## DB (database/migration/ 012~015)
- 후보 스냅샷, resolution, episode, 라벨 세션, 제외 관리 테이블 9개 + VIEW 2개

## Backend (Java)
- 12개 DTO/Controller (ParentInferenceWorkflowController 등)
- GroupPolygonService: parent_resolution LEFT JOIN + 15개 API 메서드

## Frontend
- ParentReviewPanel: 모선 검토 대시보드
- vesselAnalysis: 10개 신규 API 함수 + 6개 타입

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:42:31 +09:00

17 KiB

Gear Parent Inference Algorithm Spec

문서 목적

이 문서는 현재 구현된 어구 모선 추적 알고리즘을 모듈, 메서드, 파라미터, 판단 기준, 저장소, 엔드포인트, 영향 관계 기준으로 정리한 구현 명세다. GEAR-PARENT-INFERENCE-DATAFLOW-PAPER.md가 서술형 통합 문서라면, 이 문서는 구현과 후속 변경 작업에 바로 연결할 수 있는 참조 스펙이다.

1. 시스템 요약

1.1 현재 목적

  • 최근 24시간 한국 수역 AIS를 캐시에 유지한다.
  • 어구 이름 패턴과 위치를 기준으로 어구 그룹을 만든다.
  • 주변 선박/오분류 어구를 correlation 후보로 평가한다.
  • 후보 중 대표 모선 가능성이 높은 선박을 추론한다.
  • 사람의 라벨/제외를 별도 저장소에 남겨 향후 모델 평가와 자동화 전환에 활용한다.

1.2 현재 점수 구조의 역할 구분

  • gear_correlation_scores.current_score
    • 후보 스크리닝용 correlation score
    • EMA 기반 단기 메모리
  • gear_group_parent_candidate_snapshots.final_score
    • 모선 추론용 최종 후보 점수
    • coverage-aware 보정과 이름/안정성/episode/lineage/label prior 반영
  • gear_group_parent_resolution
    • 그룹별 현재 추론 상태
  • gear_group_episodes, gear_group_episode_snapshots
    • sub_cluster_id와 분리된 continuity memory
  • gear_parent_label_tracking_cycles
    • 라벨 세션 동안의 자동 추론 성능 추적

2. 현재 DB 저장소와 유지 기간

저장소 역할 현재 유지 규칙
group_polygon_snapshots 1h/1h-fb/6h 그룹 스냅샷 7일 cleanup
gear_correlation_raw_metrics correlation raw metric 시계열 7일 retention partition
gear_correlation_scores correlation EMA score 현재 상태 30일 미관측 시 cleanup
gear_group_parent_candidate_snapshots cycle별 parent candidate snapshot 현재 자동 cleanup 없음
gear_group_parent_resolution 그룹별 현재 추론 상태 1행 현재 자동 cleanup 없음
gear_group_episodes active/merged/expired episode 현재 상태 현재 자동 cleanup 없음
gear_group_episode_snapshots cycle별 episode continuity 스냅샷 현재 자동 cleanup 없음
gear_parent_candidate_exclusions 그룹/전역 후보 제외 기간 종료 또는 수동 해제까지
gear_parent_label_sessions 정답 라벨 세션 만료 시 EXPIRED, row는 유지
gear_parent_label_tracking_cycles 라벨 세션 cycle별 추적 현재 자동 cleanup 없음

3. 모듈 인덱스

3.1 시간/원천 적재

모듈 메서드 역할
prediction/time_bucket.py compute_safe_bucket() DB 적재 완료 전 bucket 차단
prediction/time_bucket.py compute_initial_window_start() 초기 24h window 시작점
prediction/time_bucket.py compute_incremental_window_start() overlap backfill 시작점
prediction/db/snpdb.py fetch_all_tracks() safe bucket까지 초기 bulk 적재
prediction/db/snpdb.py fetch_incremental() backfill 포함 증분 적재
prediction/cache/vessel_store.py load_initial() 초기 메모리 캐시 구성
prediction/cache/vessel_store.py merge_incremental() 증분 merge + dedupe
prediction/cache/vessel_store.py evict_stale() 24h sliding window 유지

3.2 어구 identity / 그룹

모듈 메서드 역할
prediction/fleet_tracker.py track_gear_identity() 어구 이름 파싱, identity log 관리
prediction/algorithms/gear_name_rules.py normalize_parent_name() 모선명 정규화
prediction/algorithms/gear_name_rules.py is_trackable_parent_name() 짧은 이름 제외
prediction/algorithms/polygon_builder.py detect_gear_groups() 어구 그룹 및 서브클러스터 생성
prediction/algorithms/polygon_builder.py build_all_group_snapshots() 1h/1h-fb/6h 스냅샷 저장용 생성

3.3 correlation / parent inference

모듈 메서드 역할
prediction/algorithms/gear_correlation.py run_gear_correlation() raw metric + EMA score 계산
prediction/algorithms/gear_correlation.py _compute_gear_vessel_metrics() proximity/visit/activity 계산
prediction/algorithms/gear_correlation.py update_score() EMA + freeze/decay 상태 전이
prediction/algorithms/gear_parent_episode.py build_episode_plan() continuity source와 episode assignment 계산
prediction/algorithms/gear_parent_episode.py compute_prior_bonus_components() episode/lineage/label prior bonus 계산
prediction/algorithms/gear_parent_episode.py sync_episode_states() gear_group_episodes upsert
prediction/algorithms/gear_parent_episode.py insert_episode_snapshots() episode snapshot append
prediction/algorithms/gear_parent_inference.py run_gear_parent_inference() 최종 모선 추론 실행
prediction/algorithms/gear_parent_inference.py _build_candidate_scores() 후보별 상세 점수 계산
prediction/algorithms/gear_parent_inference.py _name_match_score() 이름 점수 규칙
prediction/algorithms/gear_parent_inference.py _build_track_coverage_metrics() coverage-aware evidence 계산
prediction/algorithms/gear_parent_inference.py _select_status() 상태 전이 규칙

3.4 backend read model / workflow

모듈 메서드 역할
GroupPolygonService.java group list/review/detail SQL 최신 1h live + stale suppression read model
ParentInferenceWorkflowController.java exclusion/label API 사람 판단 저장소 API

4. 메서드 상세

4.1 prediction/time_bucket.py

compute_safe_bucket(now: datetime | None = None) -> datetime

  • 입력:
    • 현재 시각
  • 출력:
    • safe_delay를 뺀 뒤 5분 단위로 내림한 KST naive bucket
  • 기준:
    • SNPDB_SAFE_DELAY_MIN
  • 영향:
    • 초기 적재, 증분 적재, eviction 기준점

compute_incremental_window_start(last_bucket: datetime) -> datetime

  • 입력:
    • 현재 캐시의 마지막 처리 bucket
  • 출력:
    • last_bucket - SNPDB_BACKFILL_BUCKETS * 5m
  • 의미:
    • 늦게 들어온 같은 bucket row 재흡수

4.2 prediction/db/snpdb.py

fetch_all_tracks(hours: int = 24) -> pd.DataFrame

  • 역할:
    • safe bucket까지 최근 N시간 full load
  • 핵심 쿼리 조건:
    • bbox: 122,31,132,39
    • time_bucket > window_start
    • time_bucket <= safe_bucket
  • 출력 컬럼:
    • mmsi, timestamp, time_bucket, lat, lon, raw_sog

fetch_incremental(last_bucket: datetime) -> pd.DataFrame

  • 역할:
    • overlap backfill 포함 증분 load
  • 핵심 쿼리 조건:
    • time_bucket > from_bucket
    • time_bucket <= safe_bucket
  • 주의:
    • 이미 본 bucket도 일부 다시 읽는 구조다

4.3 prediction/cache/vessel_store.py

load_initial(hours: int = 24) -> None

  • 역할:
    • 초기 bulk DataFrame을 MMSI별 track cache로 구성
  • 파생 효과:
    • _last_bucket 갱신
    • static info refresh
    • permit registry refresh

merge_incremental(df_new: pd.DataFrame) -> None

  • 역할:
    • 증분 batch merge
  • 기준:
    • timestamp, time_bucket 정렬
    • timestamp 기준 dedupe
  • 영향:
    • 같은 bucket overlap backfill에서도 최종 row만 유지

evict_stale(hours: int = 24) -> None

  • 역할:
    • sliding 24h 유지
  • 기준:
    • time_bucket 있으면 bucket 기준
    • 없으면 timestamp fallback

4.4 prediction/fleet_tracker.py

track_gear_identity(gear_signals, conn) -> None

  • 역할:
    • 어구 이름 패턴에서 parent_name, gear_index_1, gear_index_2 추출
    • gear_identity_log insert/update
  • 입력:
    • gear signal list
  • 주요 기준:
    • 정규화 길이 < 4면 건너뜀
    • 같은 이름, 다른 MMSI는 identity migration 처리
  • 영향:
    • gear_correlation_scores.target_mmsi를 새 MMSI로 이전 가능

4.5 prediction/algorithms/polygon_builder.py

detect_gear_groups(vessel_store) -> list[dict]

  • 역할:
    • 어구 이름 기반 raw group 생성
    • 거리 기반 서브클러스터 분리
    • 근접 병합
  • 입력:
    • all_positions
  • 주요 기준:
    • 어구 패턴 매칭
    • 440/441 제외
    • is_trackable_parent_name()
    • MAX_DIST_DEG = 0.15
  • 출력:
    • parent_name, parent_mmsi, sub_cluster_id, members

build_all_group_snapshots(vessel_store, company_vessels, companies) -> list[dict]

  • 역할:
    • FLEET, GEAR_IN_ZONE, GEAR_OUT_ZONE1h/1h-fb/6h snapshot 생성
  • 주요 기준:
    • 같은 parent_name 전체 기준 1h active member 수
    • GEAR_OUT_ZONE 최소 멤버 수
    • parent nearby 시 isParent=true

4.6 prediction/algorithms/gear_correlation.py

run_gear_correlation(vessel_store, gear_groups, conn) -> dict

  • 역할:
    • 그룹당 후보 탐색
    • raw metric 저장
    • EMA score 갱신
  • 입력:
    • gear_groups
  • 출력:
    • updated, models, raw_inserted

_compute_gear_vessel_metrics(gear_center_lat, gear_center_lon, gear_radius_nm, vessel_track, params) -> dict

  • 출력 metric:
    • proximity_ratio
    • visit_score
    • activity_sync
    • composite
  • 한계:
    • raw metric은 짧은 항적에 과대 우호적일 수 있음
    • 이 문제는 parent inference 단계의 coverage-aware 보정에서 완화

update_score(prev_score, raw_score, streak, last_observed, now, gear_group_active_ratio, shadow_bonus, params) -> tuple

  • 상태:
    • ACTIVE
    • PATTERN_DIVERGE
    • GROUP_QUIET
    • NORMAL_GAP
    • SIGNAL_LOSS
  • 의미:
    • correlation score는 장기 기억보다 short-memory EMA에 가깝다

4.7 prediction/algorithms/gear_parent_inference.py

run_gear_parent_inference(vessel_store, gear_groups, conn) -> dict[str, int]

  • 역할:
    • direct parent 보강
    • active exclusion/label 적용
    • 후보 점수 계산
    • 상태 전이
    • snapshot/resolution/tracking 저장

_load_existing_resolution(conn, group_keys) -> dict

  • 역할:
    • 현재 그룹의 이전 resolution 상태 로드
  • 현재 쓰임:
    • PREVIOUS_SELECTION 후보 seed
    • stable_cycles
    • MANUAL_CONFIRMED 유지
    • reject cooldown

_build_candidate_scores(...) -> list[CandidateScore]

  • 후보 집합 원천:
    • 상위 correlation 후보
    • registry name exact bucket
    • previous selection
  • 제거 규칙:
    • global exclusion
    • group exclusion
    • reject cooldown
  • 점수 항목:
    • base_corr_score
    • name_match_score
    • track_similarity_score
    • visit_score_6h
    • proximity_score_6h
    • activity_sync_score_6h
    • stability_score
    • registry_bonus
    • china_mmsi_bonus 후가산

_name_match_score(parent_name, candidate_name, registry) -> float

  • 규칙:
    • 원문 동일 1.0
    • 정규화 동일 0.8
    • prefix/contains 0.5
    • 숫자 제거 후 문자 부분 동일 0.3
    • else 0.0

_build_track_coverage_metrics(center_track, vessel_track, gear_center_lat, gear_center_lon) -> dict

  • 역할:
    • short-track 과대평가 방지용 증거 강도 계산
  • 핵심 출력:
    • trackCoverageFactor
    • visitCoverageFactor
    • activityCoverageFactor
    • coverageFactor
  • downstream:
    • track, visit, proximity, activity raw score에 곱해 effective score 생성

4.8 prediction/algorithms/gear_parent_episode.py

build_episode_plan(groups, previous_by_lineage) -> EpisodePlan

  • 역할:
    • 현재 cycle group을 이전 active episode와 매칭
    • NEW, CONTINUED, SPLIT_CONTINUE, SPLIT_NEW, MERGE_NEW 결정
  • 입력:
    • GroupEpisodeInput[]
    • 최근 6h active EpisodeState[]
  • continuity score:
    • 0.75 * member_jaccard + 0.25 * center_support
  • 기준:
    • member_jaccard
    • 중심점 거리 12nm
    • continuity score threshold 0.45
    • merge score threshold 0.35
  • 출력:
    • assignment map
    • expired episode set
    • merged target map

compute_prior_bonus_components(...) -> dict[str, float]

  • 역할:
    • 동일 candidate에 대한 episode/lineage/label prior bonus 계산
  • 입력 집계 범위:
    • episode prior: 24h
    • lineage prior: 7d
    • label prior: 30d
  • cap:
    • episode <= 0.10
    • lineage <= 0.05
    • label <= 0.10
    • total <= 0.20
  • 출력:
    • episodePriorBonus
    • lineagePriorBonus
    • labelPriorBonus
    • priorBonusTotal

sync_episode_states(conn, observed_at, plan) -> None

  • 역할:
    • active/merged/expired episode 상태를 gear_group_episodes에 반영
  • 기준:
    • merge 대상은 MERGED
    • continuity 없는 old episode는 EXPIRED

insert_episode_snapshots(conn, observed_at, plan, payloads) -> int

  • 역할:
    • cycle별 continuity 결과와 top candidate/result를 gear_group_episode_snapshots에 저장
  • 기록:
    • episode_id
    • parent_episode_ids
    • top_candidate_mmsi
    • top_candidate_score
    • resolution_status

_select_status(top_candidate, margin, stable_cycles) -> tuple[str, str]

  • 상태:
    • NO_CANDIDATE
    • AUTO_PROMOTED
    • REVIEW_REQUIRED
    • UNRESOLVED
  • auto promotion 조건:
    • target_type == VESSEL
    • CORRELATION source 포함
    • final_score >= 0.72
    • margin >= 0.15
    • stable_cycles >= 3
  • review 조건:
    • final_score >= 0.60

5. 현재 엔드포인트 스펙

5.1 조회 계열

/api/kcg/vessel-analysis/groups/parent-inference/review

  • 역할:
    • 최신 전역 1h 기준 검토 대기 목록
  • 조건:
    • stale resolution 숨김
    • candidate count는 latest candidate snapshot 기준

/api/kcg/vessel-analysis/groups/{groupKey}/parent-inference

  • 역할:
    • 특정 그룹의 현재 live sub-cluster 상세
  • 주의:
    • “현재 최신 전역 1h에 실제 존재하는 sub-cluster만” 반환

/api/kcg/vessel-analysis/parent-inference/candidate-exclusions

  • 역할:
    • 그룹/전역 제외 목록 조회

/api/kcg/vessel-analysis/parent-inference/label-sessions

  • 역할:
    • active 또는 전체 라벨 세션 조회

5.2 액션 계열

POST /candidate-exclusions/global

  • 역할:
    • 전역 후보 제외 생성
  • 영향:
    • 다음 cycle부터 모든 그룹에서 해당 MMSI 제거

POST /groups/{groupKey}/parent-inference/{subClusterId}/exclude

  • 역할:
    • 그룹 단위 후보 제외 생성
  • 영향:
    • 다음 cycle부터 해당 그룹에서만 제거

POST /groups/{groupKey}/parent-inference/{subClusterId}/label

  • 역할:
    • 기간형 정답 라벨 세션 생성
  • 영향:
    • 다음 cycle부터 tracking row 누적

6. 현재 기억 구조와 prior bonus

6.1 short-memory와 long-memory의 분리

  • gear_correlation_scores
    • EMA short-memory
    • 미관측 시 decay
    • 현재 후보 seed 역할
  • gear_group_parent_resolution
    • 현재 상태 1행
    • same-episode가 아니면 PREVIOUS_SELECTION carry를 직접 사용하지 않음
  • gear_group_episodes
    • continuity memory
  • candidate_snapshots
    • bonus 집계 원천

6.2 현재 final score의 장기 기억 반영

현재는 과거 점수를 직접 carry하지 않고, 약한 prior bonus만 후가산한다.

final_score =
  current_signal_score
  + china_mmsi_bonus
  + prior_bonus_total

여기서 prior_bonus_total은:

  • episode_prior_bonus
  • lineage_prior_bonus
  • label_prior_bonus

의 합이며 총합 cap은 0.20이다.

6.3 왜 weak prior인가

과거 점수를 그대로 넘기면:

  • 다른 episode로 잘못 관성이 전이될 수 있다
  • split/merge 이후 잘못된 top1이 고착될 수 있다
  • 오래된 오답이 장기 drift로 남을 수 있다

그래서 현재 구현은 과거 점수를 “현재 score 자체”가 아니라 “작은 bonus”로만 쓴다.

7. 현재 continuity / prior 동작

7.1 episode continuity

  • 같은 lineage 안에서 최근 6h active episode를 불러온다.
  • continuity score가 높은 이전 episode가 있으면 CONTINUED
  • 1개 parent episode가 여러 current cluster로 이어지면 SPLIT_CONTINUE + SPLIT_NEW
  • 여러 previous episode가 하나 current cluster로 모이면 MERGE_NEW
  • 어떤 current와도 연결되지 못한 old episode는 EXPIRED

7.2 prior 집계

prior 참조 범위 현재 집계 값
episode prior 최근 동일 episode 24h seen_count, top1_count, avg_score, last_seen_at
lineage prior 동일 이름 lineage 7d seen_count, top1_count, top3_count, avg_score, last_seen_at
label prior 라벨 세션 30d session_count, last_labeled_at

7.3 구현 시 주의

  • 과거 점수를 직접 누적하지 말 것
  • prior는 bonus로만 사용하고 cap을 둘 것
  • split/merge 이후 parent 후보 관성은 약하게만 상속할 것
  • stale live sub-cluster와 vanished old sub-cluster를 혼동하지 않도록, aggregation도 최신 episode anchor를 기준으로 할 것

8. 참조 문서