kcg-monitoring/docs/GEAR-PARENT-INFERENCE-DATAFLOW-PAPER.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

24 KiB

Gear Parent Inference Dataflow Paper

초록

이 문서는 iran-airstrike-replay-codex의 한국 수역 어구 모선 추적 체계를 코드 기준으로 복원하는 통합 기술 문서다. 범위는 snpdb 5분 궤적 적재, 인메모리 캐시 유지, 어구 그룹 검출, 서브클러스터 생성, 1h/1h-fb/6h 폴리곤 스냅샷 저장, correlation 기반 후보 점수화, coverage-aware parent inference, episode_id 기반 연속성 계층, backend read model, review/exclusion/label v2까지 포함한다. 문서의 목적은 “현재 무엇이 구현되어 있고, 각 경우의 수에서 어떤 분기 규칙이 적용되는가”를 한 문서에서 복원 가능하게 만드는 것이다.

1. 범위와 전제

1.1 구현 기준

  • frontend: frontend/
  • backend: backend/
  • prediction: prediction/
  • schema migration: database/migration/012_gear_parent_inference.sql, database/migration/014_gear_parent_workflow_v2_phase1.sql, database/migration/015_gear_parent_episode_tracking.sql

1.2 실행 환경

  • lab backend: rocky-211:18083
  • lab prediction: redis-211:18091
  • lab schema: kcg_lab
  • 로컬 프론트 진입점: yarn dev:lab, yarn dev:lab:ssh

1.3 문서의 구분

  • 구현됨:
    • 현재 repo 코드와 lab 배포에 이미 반영된 규칙
  • 후속 확장 후보:
    • episode continuity 위에서 추가로 올릴 focus mode, richer episode lineage API, calibration report

2. 문제 정의

이 시스템은 한국 수역에서 AIS 신호를 이용해 아래 문제를 단계적으로 푼다.

  1. 최근 24시간의 선박/어구 궤적을 메모리 캐시에 유지한다.
  2. 동일한 어구 이름 계열을 공간적으로 묶어 어구 그룹을 만든다.
  3. 각 그룹에 대해 1h, 1h-fb, 6h 스냅샷을 생성한다.
  4. 주변 선박 또는 잘못 분류된 어구 AIS를 후보로 수집하고 correlation 점수를 만든다.
  5. 후보를 모선 추론 점수로 다시 환산한다.
  6. 사람이 라벨/제외를 누적해 모델 정확도 고도화용 데이터셋을 만든다.

핵심 난점은 아래 세 가지다.

  • DB 적재 지연 때문에 live incremental cache와 fresh reload가 다를 수 있다.
  • 같은 parent_name 아래에서도 실제로는 여러 공간 덩어리로 갈라질 수 있다.
  • 짧은 항적이 track/proximity/activity에서 과대평가될 수 있다.

3. 전체 아키텍처 흐름

flowchart LR
    A["signal.t_vessel_tracks_5min<br/>5분 bucket linestringM"] --> B["prediction/db/snpdb.py<br/>safe bucket + overlap backfill"]
    B --> C["prediction/cache/vessel_store.py<br/>24h in-memory cache"]
    C --> D["prediction/fleet_tracker.py<br/>gear_identity_log / snapshot"]
    C --> E["prediction/algorithms/polygon_builder.py<br/>gear group detect + sub-cluster + snapshots"]
    E --> F["kcg_lab.group_polygon_snapshots"]
    C --> G["prediction/algorithms/gear_correlation.py<br/>raw metrics + EMA score"]
    G --> H["kcg_lab.gear_correlation_raw_metrics"]
    G --> I["kcg_lab.gear_correlation_scores"]
    F --> J["prediction/algorithms/gear_parent_inference.py<br/>candidate build + scoring + status"]
    H --> J
    I --> J
    K["v2 exclusions / labels"] --> J
    J --> L["kcg_lab.gear_group_parent_candidate_snapshots"]
    J --> M["kcg_lab.gear_group_parent_resolution"]
    J --> N["kcg_lab.gear_parent_label_tracking_cycles"]
    F --> O["backend GroupPolygonService"]
    L --> O
    M --> O
    N --> O
    O --> P["frontend ParentReviewPanel"]

4. 원천 데이터와 시간 모델

4.1 원천 데이터 형식

원천은 signal.t_vessel_tracks_5min이며, 1 row = 1 MMSI = 5분 구간의 궤적 전체LineStringM으로 보관한다. 실제 위치 포인트는 ST_DumpPoints(track_geom)로 분해하고, 각 점의 timestamp는 ST_M((dp).geom)에서 꺼낸다. 구현 위치는 prediction/db/snpdb.py다.

4.2 safe watermark

현재 구현은 “DB 적재가 완료된 bucket만 읽는다”는 원칙을 따른다.

  • prediction/time_bucket.py
    • compute_safe_bucket()
    • compute_initial_window_start()
    • compute_incremental_window_start()
  • 기본값:
    • SNPDB_SAFE_DELAY_MIN
    • SNPDB_BACKFILL_BUCKETS

핵심 규칙:

  1. 초기 적재는 now - safe_delay를 5분 내림한 safe_bucket까지만 읽는다.
  2. 증분 적재는 last_bucket - backfill_window부터 safe_bucket까지 다시 읽는다.
  3. live cache는 timestamp가 아니라 time_bucket 기준으로 24시간 cutoff를 맞춘다.

4.3 왜 safe watermark가 필요한가

time_bucket > last_bucket만 사용하면, 늦게 들어온 같은 bucket row를 영구히 놓칠 수 있다. 현재 구현은 overlap backfill과 dedupe로 이 drift를 줄인다.

  • 조회: prediction/db/snpdb.py
  • 병합 dedupe: prediction/cache/vessel_store.py

5. Stage 1: 캐시 적재와 유지

5.1 초기 적재

prediction/main.py는 시작 시 vessel_store.load_initial(24)를 호출한다.

prediction/cache/vessel_store.py의 규칙:

  1. snpdb.fetch_all_tracks(hours)로 최근 24시간을 safe bucket까지 읽는다.
  2. MMSI별 DataFrame으로 _tracks를 구성한다.
  3. 최대 time_bucket_last_bucket으로 저장한다.
  4. static info와 permit registry를 함께 refresh한다.

5.2 증분 병합

스케줄러는 snpdb.fetch_incremental(vessel_store.last_bucket)로 overlap backfill 구간을 다시 읽는다.

merge_incremental() 규칙:

  1. 기존 DataFrame과 새 batch를 합친다.
  2. timestamp, time_bucket으로 정렬한다.
  3. timestamp 기준 중복은 keep='last'로 제거한다.
  4. batch의 최대 time_bucket이 더 크면 _last_bucket을 갱신한다.

5.3 stale eviction

evict_stale()는 safe bucket 기준 24시간 이전 포인트를 제거한다. time_bucket이 있으면 bucket 기준, 없으면 timestamp 기준으로 fallback한다.

6. Stage 2: 어구 identity 추출

prediction/fleet_tracker.py는 어구 이름 패턴에서 parent_name, gear_index_1, gear_index_2를 파싱하고 gear_identity_log를 관리한다.

6.1 이름 기반 필터

공통 규칙은 prediction/algorithms/gear_name_rules.py에 있다.

  • 정규화:
    • 대문자화
    • 공백, _, -, % 제거
  • 추적 가능 최소 길이:
    • 정규화 길이 >= 4

fleet_tracker.pypolygon_builder.py는 모두 is_trackable_parent_name()을 사용한다. 즉 짧은 이름은 추론 이전, 그룹 생성 이전 단계부터 제외된다.

6.2 identity log 동작

fleet_tracker.py의 핵심 분기:

  1. 같은 MMSI + 같은 이름:
  • 기존 활성 row의 last_seen_at, 위치만 갱신
  1. 같은 MMSI + 다른 이름:
  • 기존 row 비활성화
  • 새 row insert
  1. 다른 MMSI + 같은 이름:
  • 기존 row 비활성화
  • 새 MMSI로 row insert
  • 기존 gear_correlation_scores.target_mmsi를 새 MMSI로 이전

7. Stage 3: 어구 그룹 생성과 서브클러스터

실제 어구 그룹은 prediction/algorithms/polygon_builder.pydetect_gear_groups()가 만든다.

7.1 1차 그룹화

규칙:

  1. 최신 position 이름이 어구 패턴에 맞아야 한다.
  2. STALE_SEC를 넘는 오래된 신호는 제외한다.
  3. 440, 441 MMSI는 어구 AIS 미사용으로 간주해 제외한다.
  4. is_trackable_parent_name(parent_raw)를 만족해야 한다.
  5. 같은 parent_name은 공백 제거 버전으로 묶는다.

7.2 서브클러스터 생성

같은 이름 아래에서도 거리 기반 연결성으로 덩어리를 나눈다.

  • 거리 임계치: MAX_DIST_DEG = 0.15
  • 연결 규칙:
    • 각 어구가 클러스터 내 최소 1개와 MAX_DIST_DEG 이내면 같은 연결 요소
  • 구현:
    • Union-Find

모선이 이미 있으면, 모선과 가장 가까운 클러스터를 seed cluster로 간주한다.

7.3 sub_cluster_id 부여 규칙

현재 구현은 아래와 같다.

  1. 클러스터가 1개면 sub_cluster_id = 0
  2. 클러스터가 여러 개면 1..N
  3. 이후 동일 parent_key의 두 서브그룹이 다시 근접 병합되면 sub_cluster_id = 0

sub_cluster_id는 영구 식별자가 아니라 “그 시점의 공간 분리 라벨”이다.

7.4 병합 규칙

동일 parent_key의 두 그룹이 다시 가까워지면:

  1. 멤버를 합친다.
  2. 부모 MMSI가 없는 큰 그룹에 작은 그룹의 parent_mmsi를 승계할 수 있다.
  3. sub_cluster_id = 0으로 재설정한다.

7.5 스냅샷 생성 규칙

build_all_group_snapshots()는 각 그룹에 대해 1h, 1h-fb, 6h 스냅샷을 만든다.

  • 1h
    • 같은 parent_name 전체 기준 1시간 활성 멤버 수 >= 2
  • 1h-fb
    • 같은 parent_name 전체 기준 1시간 활성 멤버 수 < 2
    • 리플레이/일치율 추적용
    • 라이브 현황에서 제외
  • 6h
    • 6시간 내 stale이 아니어야 함

추가 규칙:

  1. 서브클러스터 내 1h 활성 멤버가 2개 미만이면 최신 2개로 fallback display를 만든다.
  2. 수역 외(GEAR_OUT_ZONE)인데 멤버 수가 MIN_GEAR_GROUP_SIZE 미만이면 스킵한다.
  3. 모선이 있고, 멤버와 충분히 근접하면 members[].isParent = true로 같이 넣는다.

8. Stage 4: correlation 모델

prediction/algorithms/gear_correlation.py는 어구 그룹별 raw metric과 EMA score를 만든다.

8.1 후보 생성

입력:

  • group center
  • group radius
  • active ratio
  • group member MMSI set

출력 후보:

  • 선박 후보(VESSEL)
  • 잘못 분류된 어구 후보(GEAR_BUOY)

후보 수는 그룹당 최대 30개로 제한된다.

8.2 raw metric

선박 후보는 최근 6시간 항적 기반으로 아래 값을 만든다.

  • proximity_ratio
  • visit_score
  • activity_sync
  • dtw_similarity

어구 후보는 단순 거리 기반 proximity_ratio만 사용한다.

8.3 EMA score

모델 파라미터(gear_correlation_param_models)별로 아래를 수행한다.

  1. composite score 계산
  2. 이전 score와 streak를 읽는다
  3. update_score()로 EMA 갱신
  4. threshold 이상이거나 기존 row가 있으면 upsert

반대로 이번 사이클 후보군에서 빠진 기존 항목은 OUT_OF_RANGE로 fast decay된다.

8.4 correlation 산출물

  • gear_correlation_raw_metrics
  • gear_correlation_scores

여기까지는 “잠재적 모선/근접 대상”의 score이고, 최종 parent inference는 아직 아니다.

9. Stage 5: parent inference

prediction/algorithms/gear_parent_inference.py가 최종 모선 추론을 수행한다.

전체 진입점은 run_gear_parent_inference(vessel_store, gear_groups, conn)이다.

9.1 전체 분기 개요

flowchart TD
    A["active gear group"] --> B{"direct parent member<br/>exists?"}
    B -- yes --> C["DIRECT_PARENT_MATCH<br/>fresh resolution upsert"]
    B -- no --> D{"trackable parent name?"}
    D -- no --> E["SKIPPED_SHORT_NAME"]
    D -- yes --> F["build candidate set"]
    F --> G{"candidate exists?"}
    G -- no --> H["NO_CANDIDATE"]
    G -- yes --> I["score + rank + margin + stable cycles"]
    I --> J{"auto promotion rule?"}
    J -- yes --> K["AUTO_PROMOTED"]
    J -- no --> L{"top score >= 0.60?"}
    L -- yes --> M["REVIEW_REQUIRED"]
    L -- no --> N["UNRESOLVED"]

9.1.1 episode continuity 선행 단계

현재 구현에서 run_gear_parent_inference()는 후보 점수를 만들기 전에 먼저 prediction/algorithms/gear_parent_episode.py를 호출해 active 그룹의 continuity를 계산한다.

입력:

  • 현재 cycle gear_groups
  • 정규화된 parent_name
  • 최근 6h active gear_group_episodes
  • 최근 24h episode prior, 7d lineage prior, 30d label prior 집계

핵심 규칙:

  1. continuity score는 0.75 * member_jaccard + 0.25 * center_support다.
  2. 중심점 지원값은 12nm 이내일수록 커진다.
  3. continuity score가 충분하거나, overlap member가 있고 거리 조건을 만족하면 연결 후보로 본다.
  4. 두 개 이상 active episode가 하나의 현재 cluster로 들어오면 MERGE_NEW다.
  5. 하나의 episode가 여러 현재 cluster로 갈라지면 하나는 SPLIT_CONTINUE, 나머지는 SPLIT_NEW다.
  6. 아무 previous episode와도 연결되지 않으면 NEW다.
  7. 현재 cycle과 연결되지 못한 active episode는 EXPIRED 또는 MERGED로 종료한다.

현재 저장되는 continuity 메타데이터:

  • gear_group_parent_candidate_snapshots.episode_id
  • gear_group_parent_resolution.episode_id
  • gear_group_parent_resolution.continuity_source
  • gear_group_parent_resolution.continuity_score
  • gear_group_parent_resolution.prior_bonus_total
  • gear_group_episodes
  • gear_group_episode_snapshots

9.2 direct parent 보강

최신 어구 그룹에 아래 중 하나가 있으면 후보 추론 대신 직접 모선 매칭으로 처리한다.

  1. members[].isParent = true
  2. group.parent_mmsi 존재

이 경우:

  • status = DIRECT_PARENT_MATCH
  • decision_source = DIRECT_PARENT_MATCH
  • confidence = 1.0
  • candidateCount = 0

단, 기존 상태가 MANUAL_CONFIRMED면 그 수동 상태를 유지한다.

9.3 짧은 이름 스킵

정규화 이름 길이 < 4면:

  • 후보 생성 자체를 수행하지 않는다.
  • status = SKIPPED_SHORT_NAME
  • decision_source = AUTO_SKIP

9.4 후보 집합

후보 집합은 아래의 합집합이다.

  1. default correlation model 상위 후보
  2. registry name exact bucket
  3. 기존 resolution의 selected_parent_mmsi 또는 이전 top candidate

여기에 아래를 적용한다.

  • active global exclusion 제거
  • active group exclusion 제거
  • 최근 reject cooldown 후보 제거

9.5 이름 점수

현재 구현 규칙:

  1. 원문 완전일치: 1.0
  2. 정규화 완전일치: 0.8
  3. prefix/contains: 0.5
  4. 숫자를 제거한 순수 문자 부분만 동일: 0.3
  5. 그 외: 0.0

비교 대상:

  • parent_name
  • 후보 AIS 이름
  • registry name_cn
  • registry name_en

9.6 coverage-aware evidence

짧은 항적 과대평가를 막기 위해 raw score와 effective score를 분리한다.

evidence에 남는 값:

  • trackPointCount
  • trackSpanMinutes
  • overlapPointCount
  • overlapSpanMinutes
  • inZonePointCount
  • inZoneSpanMinutes
  • trackCoverageFactor
  • visitCoverageFactor
  • activityCoverageFactor
  • coverageFactor

현재 최종 점수에는 raw가 아니라 adjusted score가 들어간다.

9.7 점수 식

가중치 합은 아래다.

  • 0.40 * base_corr
  • 0.15 * name_match
  • 0.15 * track_similarity_effective
  • 0.10 * visit_effective
  • 0.05 * proximity_effective
  • 0.05 * activity_effective
  • 0.10 * stability
  • + registry_bonus(0.05)

그 다음 별도 후가산:

  • 412/413 MMSI 보너스 +0.15
  • 단, preBonusScore >= 0.30일 때만 적용
  • episode/lineage/label prior bonus
    • 최근 동일 episode 24h
    • 동일 lineage 7d
    • 라벨 세션 30d
    • 총합 cap 0.20

9.8 상태 전이

분기 조건:

  • NO_CANDIDATE
    • 후보가 하나도 없을 때
  • AUTO_PROMOTED
    • target_type == VESSEL
    • candidate source에 CORRELATION 포함
    • final_score >= auto_promotion_threshold
    • margin >= auto_promotion_margin
    • stable_cycles >= auto_promotion_stable_cycles
  • REVIEW_REQUIRED
    • final_score >= 0.60
  • UNRESOLVED
    • 나머지

추가 예외:

  • 기존 상태가 MANUAL_CONFIRMED면 수동 상태를 유지한다.
  • active label session이 있으면 tracking row를 별도로 적재한다.

9.9 산출물

  • gear_group_parent_candidate_snapshots
  • gear_group_parent_resolution
  • gear_parent_label_tracking_cycles
  • gear_group_episodes
  • gear_group_episode_snapshots

10. Stage 6: backend read model

backend의 중심은 backend/.../GroupPolygonService.java다.

10.1 최신 1h만 라이브로 간주

group list, review queue, detail API는 모두 최신 전역 1h 스냅샷만 기준으로 삼는다.

핵심 효과:

  1. 1h-fb는 라이브 현황에서 기본 제외된다.
  2. 이미 사라진 과거 sub-cluster는 detail API에서 다시 보이지 않는다.

10.2 stale inference 차단

resolution.last_evaluated_at >= group.snapshot_time인 경우만 join한다.

즉 최신 group snapshot보다 오래된 candidate/resolution은 detail/review/list에서 숨긴다. 이 규칙이 ZHEDAIYU02433, ZHEDAIYU02394 유형 stale 표시를 막는다.

10.3 detail API 의미

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

현재 의미:

  • 해당 그룹의 최신 전역 1h live sub-cluster 집합
  • 각 sub-cluster의 fresh resolution
  • 각 sub-cluster의 latest candidate snapshot

11. Stage 7: review / exclusion / label v2

v2 Phase 1은 “자동 추론 결과”와 “사람 판단 데이터”를 분리하는 구조다.

11.1 사람 판단 저장소

  • gear_parent_candidate_exclusions
  • gear_parent_label_sessions
  • gear_parent_label_tracking_cycles

11.2 액션 의미

  • 그룹 제외:
    • 특정 group_key + sub_cluster_id에서 특정 후보 MMSI를 일정 기간 제거
  • 전체 후보 제외:
    • 특정 MMSI를 모든 그룹 후보군에서 제거
  • 정답 라벨:
    • 특정 그룹에 대해 정답 parent MMSI를 1/3/5일 세션으로 지정
    • prediction은 이후 cycle마다 top1/top3 여부를 추적

11.3 why v2

기존 MANUAL_CONFIRMED/REJECT는 운영 override 성격이 강했고, “모델 정확도 평가용 백데이터”와 섞였다. v2는 이 둘을 분리해 라벨을 평가 데이터로 쓰도록 한다.

12. 실제 경우의 수 분기표

경우 구현 위치 현재 동작
이름 길이 < 4 gear_name_rules.py, fleet_tracker.py, polygon_builder.py, gear_parent_inference.py identity/grouping/inference 단계에서 제외 또는 SKIPPED_SHORT_NAME
직접 모선 포함 polygon_builder.py, gear_parent_inference.py DIRECT_PARENT_MATCH fresh resolution
같은 이름, 멀리 떨어진 어구 polygon_builder.py 별도 sub-cluster 생성
두 서브클러스터가 다시 근접 polygon_builder.py 하나로 병합, sub_cluster_id = 0
group 전체 1h 활성 멤버 < 2 polygon_builder.py 1h-fb 생성, live 현황 제외
후보가 하나도 없음 gear_parent_inference.py NO_CANDIDATE
짧은 항적이 우연히 근접 gear_parent_inference.py coverage-aware 보정으로 effective score 감소
stale old inference가 남아 있음 GroupPolygonService.java 최신 group snapshot보다 오래되면 숨김
직접 parent가 이미 있음 gear_parent_inference.py 후보 계산 대신 direct parent resolution

13. sub_cluster_id의 한계

현재 코드에서 sub_cluster_id는 영구 identity가 아니다.

이유:

  1. 같은 이름 그룹의 공간 분리 수가 cycle마다 달라질 수 있다.
  2. 병합되면 0으로 재설정된다.
  3. 멤버가 추가/이탈해도 기존 번호 의미가 유지된다고 보장할 수 없다.

따라서 group_key + sub_cluster_id는 “현재 cycle의 공간 덩어리”를 가리키는 키로는 유효하지만, 장기 연속 추적 키로는 부적합하다.

14. Stage 8: episode_id continuity + prior bonus

14.1 목적

현재 구현의 episode_id는 “같은 어구 덩어리의 시간적 연속성”을 추적하는 별도 식별자다. sub_cluster_id를 대체하지 않고, 그 위에 얹는 계층이다.

핵심 목적:

  • 작은 멤버 변화는 같은 episode로 이어 붙인다.
  • 구조적 split/merge는 continuity source로 기록한다.
  • long-memory는 stable_cycles 직접 승계가 아니라 약한 prior bonus로만 전달한다.

14.2 현재 저장소

  • gear_group_episodes
    • active/merged/expired episode 현재 상태
  • gear_group_episode_snapshots
    • cycle별 episode 스냅샷
  • gear_group_parent_candidate_snapshots
    • episode_id, normalized_parent_name, episode_prior_bonus, lineage_prior_bonus, label_prior_bonus
  • gear_group_parent_resolution
    • episode_id, continuity_source, continuity_score, prior_bonus_total

14.3 continuity score

현재 continuity score는 아래다.

continuity_score =
  0.75 * member_jaccard
  + 0.25 * center_support
  • member_jaccard
    • 현재/이전 episode 멤버 MMSI Jaccard
  • center_support
    • 중심점 거리 12nm 이내일수록 높아지는 값

연결 후보 판단:

  • continuity score >= 0.45
  • 또는 overlap member가 있고 거리 조건을 만족하면 연결 후보로 인정

14.4 continuity source 규칙

  • NEW
    • 어떤 이전 episode와도 연결되지 않음
  • CONTINUED
    • 1:1 continuity
  • SPLIT_CONTINUE
    • 하나의 이전 episode가 여러 현재 cluster로 갈라졌고, 그중 주 가지
  • SPLIT_NEW
    • split로 새로 생성된 가지
  • MERGE_NEW
    • 2개 이상 active episode가 의미 있게 하나의 현재 cluster로 합쳐짐
  • DIRECT_PARENT_MATCH
    • 직접 모선 포함 그룹이 fresh resolution으로 정리되는 경우의 최종 resolution source

14.5 merge / split / expire

현재 구현 규칙:

  1. split
  • 가장 유사한 현재 cluster 1개는 기존 episode 유지
  • 나머지는 새 episode 생성
  • 새 episode에는 split_from_episode_id 저장
  1. merge
  • 2개 이상 previous episode가 같은 현재 cluster로 의미 있게 들어오면 새 episode 생성
  • 이전 episode들은 MERGED, merged_into_episode_id = 새 episode
  1. expire
  • 최근 6h active episode가 현재 cycle 어떤 cluster와도 연결되지 않으면 EXPIRED

14.6 prior bonus 계층

현재 final score에는 signal score 뒤에 아래 prior bonus가 후가산된다.

  • episode_prior_bonus
    • 최근 동일 episode 24h
    • cap 0.10
  • lineage_prior_bonus
    • 동일 정규화 이름 lineage 7d
    • cap 0.05
  • label_prior_bonus
    • 동일 lineage 라벨 세션 30d
    • cap 0.10
  • 총합 cap
    • 0.20

현재 후보가 이미 candidate set에 들어온 경우에만 적용하며, 과거 점수를 직접 carry하는 대신 약한 보너스로만 사용한다.

14.7 병합 후 후보 관성

질문 사례처럼 A episode 후보 a, B episode 후보 b가 있다가 병합 후 b가 더 적합해질 수 있다. 현재 구현은 병합 시 무조건 A를 유지하지 않고 새 episode를 생성해 A/B 둘 다의 history를 prior bonus 풀에서 재평가한다. 따라서 b는 완전 신규 후보처럼 0에서 시작하지 않지만, A의 과거 stable_cycles가 그대로 지배하지도 않는다.

15. 현재 episode 상태 흐름

stateDiagram-v2
    [*] --> Active
    Active --> Active: "CONTINUED / 소규모 멤버 변동"
    Active --> Active: "SPLIT_CONTINUE"
    Active --> Active: "MERGE_NEW로 새 episode 생성 후 연결"
    Active --> Merged: "merged_into_episode_id 기록"
    Active --> Expired: "최근 6h continuity 없음"
    Merged --> [*]
    Expired --> [*]

16. 결론

현재 구현은 아래를 모두 포함한다.

  • safe watermark + overlap backfill 기반 incremental 안정화
  • 짧은 이름 그룹 제거
  • 거리 기반 sub-cluster와 1h/1h-fb/6h 스냅샷
  • correlation + parent inference 분리
  • coverage-aware score 보정
  • stale inference 차단
  • direct parent supplement
  • v2 exclusion/label/tracking 저장소
  • episode_id continuity와 prior bonus

남은 과제는 episode 자체보다, 이 continuity 계층을 read model과 시각화에서 더 설명력 있게 노출하는 것이다. 즉 다음 단계의 핵심은 episode 도입이 아니라, episode lineage API, calibration report, richer review analytics를 얹는 일이다.

17. 참고 코드

  • prediction/main.py
  • prediction/time_bucket.py
  • prediction/db/snpdb.py
  • prediction/cache/vessel_store.py
  • prediction/fleet_tracker.py
  • prediction/algorithms/gear_name_rules.py
  • prediction/algorithms/polygon_builder.py
  • prediction/algorithms/gear_correlation.py
  • prediction/algorithms/gear_parent_episode.py
  • prediction/algorithms/gear_parent_inference.py
  • backend/src/main/java/gc/mda/kcg/domain/fleet/GroupPolygonService.java
  • backend/src/main/java/gc/mda/kcg/domain/fleet/ParentInferenceWorkflowController.java
  • database/migration/012_gear_parent_inference.sql
  • database/migration/014_gear_parent_workflow_v2_phase1.sql
  • database/migration/015_gear_parent_episode_tracking.sql