iran prediction 47개 Python 파일을 prediction/ 디렉토리로 복제: - algorithms/ 14개 분석 알고리즘 (어구추론, 다크베셀, 스푸핑, 환적, 위험도 등) - pipeline/ 7단계 분류 파이프라인 - cache/vessel_store (24h 슬라이딩 윈도우) - db/ 어댑터 (snpdb 원본조회, kcgdb 결과저장) - chat/ AI 채팅 (Ollama, 후순위) - data/ 정적 데이터 (기선, 특정어업수역 GeoJSON) config.py를 kcgaidb로 재구성 (DB명, 사용자, 비밀번호) DB 연결 검증 완료 (kcgaidb 37개 테이블 접근 확인) Makefile에 dev-prediction / dev-all 타겟 추가 CLAUDE.md에 prediction 섹션 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
83 lines
2.5 KiB
Python
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)
|