kcg-monitoring/prediction/algorithms/spoofing.py
htlee 7573c84e91 fix: 분석 파이프라인 정확도 개선 + 캐시 증분 갱신 + TTS 프록시
- MIN_TRAJ_POINTS 100→20 (16척→684척, 파이프라인 병목 해소)
- risk.py: SOG 급변 count를 위험도 점수에 반영 (+5/+10)
- spoofing.py: BD09 오프셋 중국 MMSI(412*) 예외 (좌표계 노이즈 제거)
- fishing_pattern.py: 마지막 조업 세그먼트 누락 버그 수정
- VesselAnalysisService: 인메모리 캐시 + 증분 갱신 (warmup 2h → incremental)
- nginx: /api/gtts 프록시 추가 (Google TTS CORS 우회)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 06:48:27 +09:00

83 lines
2.5 KiB
Python

import pandas as pd
from algorithms.location import haversine_nm, bd09_to_wgs84, compute_bd09_offset # noqa: F401
MAX_FISHING_SPEED_KNOTS = 25.0
def detect_teleportation(df_vessel: pd.DataFrame,
max_speed_knots: float = MAX_FISHING_SPEED_KNOTS) -> list[dict]:
"""연속 AIS 포인트 간 물리적 불가능 이동 탐지."""
if len(df_vessel) < 2:
return []
anomalies = []
records = df_vessel.sort_values('timestamp').to_dict('records')
for i in range(1, len(records)):
prev, curr = records[i - 1], records[i]
dist_nm = haversine_nm(prev['lat'], prev['lon'], curr['lat'], curr['lon'])
dt_hours = (
pd.Timestamp(curr['timestamp']) - pd.Timestamp(prev['timestamp'])
).total_seconds() / 3600
if dt_hours <= 0:
continue
implied_speed = dist_nm / dt_hours
if implied_speed > max_speed_knots:
anomalies.append({
'idx': i,
'dist_nm': round(dist_nm, 2),
'implied_kn': round(implied_speed, 1),
'type': 'TELEPORTATION',
'confidence': 'HIGH' if implied_speed > 50 else 'MED',
})
return anomalies
def count_speed_jumps(df_vessel: pd.DataFrame, threshold_knots: float = 10.0) -> int:
"""연속 SOG 급변 횟수."""
if len(df_vessel) < 2:
return 0
sog = df_vessel['sog'].values
jumps = 0
for i in range(1, len(sog)):
if abs(sog[i] - sog[i - 1]) > threshold_knots:
jumps += 1
return jumps
def compute_spoofing_score(df_vessel: pd.DataFrame) -> float:
"""종합 GPS 스푸핑 점수 (0~1)."""
if len(df_vessel) < 2:
return 0.0
score = 0.0
n = len(df_vessel)
# 순간이동 비율
teleports = detect_teleportation(df_vessel)
if teleports:
score += min(0.4, len(teleports) / n * 10)
# SOG 급변 비율
jumps = count_speed_jumps(df_vessel)
if jumps > 0:
score += min(0.3, jumps / n * 5)
# BD09 오프셋 — 중국 선박(412*)은 좌표계 차이로 항상 ~300m이므로 제외
mmsi_str = str(df_vessel.iloc[0].get('mmsi', '')) if 'mmsi' in df_vessel.columns else ''
if not mmsi_str.startswith('412'):
mid_idx = len(df_vessel) // 2
row = df_vessel.iloc[mid_idx]
offset = compute_bd09_offset(row['lat'], row['lon'])
if offset > 300:
score += 0.3
elif offset > 100:
score += 0.1
return round(min(score, 1.0), 4)