kcg-monitoring/prediction/algorithms/dark_vessel.py
htlee a68dfb21b2 feat: Python 어선 분류기 + 배포 설정 + 백엔드 모니터링 프록시
- prediction/: FastAPI 7단계 분류 파이프라인 + 6개 탐지 알고리즘
  - snpdb 궤적 조회 → 인메모리 캐시(13K척) → 분류 → kcgdb 저장
  - APScheduler 5분 주기, Python 3.9 호환
  - 버그 수정: @property last_bucket, SQL INTERVAL 바인딩, rollback, None 가드
  - 보안: DB 비밀번호 하드코딩 제거 → env 환경변수 필수
- deploy/kcg-prediction.service: systemd 서비스 (redis-211, 포트 8001)
- deploy.yml: prediction CI/CD 배포 단계 추가 (192.168.1.18:32023)
- backend: PredictionProxyController (health/status/trigger 프록시)
- backend: AppProperties predictionBaseUrl + AuthFilter 인증 예외

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:10:21 +09:00

60 lines
1.6 KiB
Python

import pandas as pd
from algorithms.location import haversine_nm
GAP_SUSPICIOUS_SEC = 1800 # 30분
GAP_HIGH_SUSPICIOUS_SEC = 3600 # 1시간
GAP_VIOLATION_SEC = 86400 # 24시간
def detect_ais_gaps(df_vessel: pd.DataFrame) -> list[dict]:
"""AIS 수신 기록에서 소실 구간 추출."""
if len(df_vessel) < 2:
return []
gaps = []
records = df_vessel.sort_values('timestamp').to_dict('records')
for i in range(1, len(records)):
prev, curr = records[i - 1], records[i]
prev_ts = pd.Timestamp(prev['timestamp'])
curr_ts = pd.Timestamp(curr['timestamp'])
gap_sec = (curr_ts - prev_ts).total_seconds()
if gap_sec < GAP_SUSPICIOUS_SEC:
continue
disp = haversine_nm(
prev['lat'], prev['lon'],
curr['lat'], curr['lon'],
)
if gap_sec >= GAP_VIOLATION_SEC:
severity = 'VIOLATION'
elif gap_sec >= GAP_HIGH_SUSPICIOUS_SEC:
severity = 'HIGH_SUSPICIOUS'
else:
severity = 'SUSPICIOUS'
gaps.append({
'gap_sec': int(gap_sec),
'gap_min': round(gap_sec / 60, 1),
'displacement_nm': round(disp, 2),
'severity': severity,
})
return gaps
def is_dark_vessel(df_vessel: pd.DataFrame) -> tuple[bool, int]:
"""다크베셀 여부 판정.
Returns: (is_dark, max_gap_duration_min)
"""
gaps = detect_ais_gaps(df_vessel)
if not gaps:
return False, 0
max_gap_min = max(g['gap_min'] for g in gaps)
is_dark = max_gap_min >= 30 # 30분 이상 소실
return is_dark, int(max_gap_min)