feat(prediction): 경량 분석 riskScore 해상도 개선 + vessel_type 매핑
경량 경로 선박 60.8%가 45점 고정으로 수렴하고 98.6%가 vessel_type UNKNOWN 으로만 기록되던 문제를 해결한다. riskScore (compute_lightweight_risk_score) - dark_suspicion_score(0~100) 직접 반영: min(30, score*0.3) - EEZ_OR_BEYOND 기선 근접도 가산 (12NM 내 +15, 24NM 내 +8) - dark_history_24h 가산 (dark_suspicion_score 미반영 케이스만) - 허가 이력 +20 → +8/+15 차등 (dark_suspicion_score 있을 때 이중계산 방지) - gap_duration_min 4구간 차등 (fallback: 720m/180m/60m/30m) vessel_type (신규 vessel_type_mapping.py) - fleet_vessels fishery_code → VesselType 매핑: PT/PT-S/OT → TRAWL, GN → GILLNET, PS → PURSE, FC → CARGO - GILLNET / CARGO 2개 값 신규 추가 (기존 TRAWL/PURSE/LONGLINE/TRAP/UNKNOWN) - scheduler.py 경량 경로에서 등록선은 매핑, 미등록선은 UNKNOWN 유지 배포 후 검증 (redis-211 15:15 사이클) - risk_score 분포: 45점 60.8% → 0% (11~40 범위 고르게 분산) - vessel_type: UNKNOWN 98.6% → 89.1% (886척이 구체 유형으로 전환, TRAWL 518 / LONGLINE 171 / TRAP 78 / PURSE 73 / GILLNET 38 / CARGO 8) - 412354335 샘플: 45 MEDIUM 고정 → 20 LOW (dss=40 × 0.3 + 축소 허가)
This commit is contained in:
부모
1def64dd1d
커밋
6fb0b04992
@ -14,42 +14,71 @@ def compute_lightweight_risk_score(
|
||||
is_dark: bool = False,
|
||||
gap_duration_min: int = 0,
|
||||
spoofing_score: float = 0.0,
|
||||
dark_suspicion_score: int = 0,
|
||||
dist_from_baseline_nm: float = 999.0,
|
||||
dark_history_24h: int = 0,
|
||||
) -> Tuple[int, str]:
|
||||
"""위치·허가·다크/스푸핑 기반 경량 위험도 (파이프라인 미통과 선박용).
|
||||
|
||||
pipeline path의 compute_vessel_risk_score와 동일한 임계값(70/50/30)을 사용해
|
||||
분류 결과의 일관성을 유지한다. dark/spoofing 신호를 추가하여 max 100점 도달 가능.
|
||||
compute_dark_suspicion 의 패턴 기반 0~100 점수를 직접 반영해 해상도를 높인다.
|
||||
이중계산 방지: dark_suspicion_score 는 이미 무허가/반복을 포함하므로 dark_suspicion_score > 0
|
||||
인 경우 허가/반복 가산을 축소한다.
|
||||
|
||||
임계값 70/50/30 은 pipeline path(compute_vessel_risk_score)와 동일.
|
||||
|
||||
Returns: (risk_score, risk_level)
|
||||
"""
|
||||
score = 0
|
||||
|
||||
# 1. 위치 기반 (최대 40점)
|
||||
# 1. 위치 기반 (최대 40점) — EEZ 외 기선 근접도 추가
|
||||
zone = zone_info.get('zone', '')
|
||||
if zone == 'TERRITORIAL_SEA':
|
||||
score += 40
|
||||
elif zone == 'CONTIGUOUS_ZONE':
|
||||
score += 10
|
||||
score += 15
|
||||
elif zone.startswith('ZONE_'):
|
||||
if is_permitted is not None and not is_permitted:
|
||||
score += 25
|
||||
elif zone == 'EEZ_OR_BEYOND':
|
||||
# EEZ 외라도 기선 근접 시 가산 (공해·외해 분산)
|
||||
if dist_from_baseline_nm < 12:
|
||||
score += 15
|
||||
elif dist_from_baseline_nm < 24:
|
||||
score += 8
|
||||
|
||||
# 2. 다크 베셀 (최대 25점)
|
||||
# 2. 다크 베셀 (최대 30점) — dark_suspicion_score 우선
|
||||
if is_dark:
|
||||
if gap_duration_min >= 60:
|
||||
score += 25
|
||||
elif gap_duration_min >= 30:
|
||||
score += 10
|
||||
if dark_suspicion_score >= 1:
|
||||
# compute_dark_suspicion 이 산출한 패턴 기반 의심도 반영
|
||||
score += min(30, round(dark_suspicion_score * 0.3))
|
||||
else:
|
||||
# fallback: gap 길이만 기준
|
||||
if gap_duration_min >= 720:
|
||||
score += 25
|
||||
elif gap_duration_min >= 180:
|
||||
score += 20
|
||||
elif gap_duration_min >= 60:
|
||||
score += 15
|
||||
elif gap_duration_min >= 30:
|
||||
score += 8
|
||||
|
||||
# 3. 스푸핑 (최대 15점)
|
||||
# 3. 스푸핑 (최대 15점) — 현재 중국 선박은 거의 0 (별도 PR 에서 산출 로직 재설계 예정)
|
||||
if spoofing_score > 0.7:
|
||||
score += 15
|
||||
elif spoofing_score > 0.5:
|
||||
score += 8
|
||||
|
||||
# 4. 허가 이력 (최대 20점)
|
||||
# 4. 허가 이력 (최대 15점) — 이중계산 방지
|
||||
if is_permitted is not None and not is_permitted:
|
||||
score += 20
|
||||
# dark_suspicion_score 에 이미 무허가 +10 반영됨 → 축소
|
||||
score += 8 if dark_suspicion_score > 0 else 15
|
||||
|
||||
# 5. 반복 이력 (최대 10점) — dark_suspicion_score 미반영 케이스만
|
||||
if dark_suspicion_score == 0 and dark_history_24h > 0:
|
||||
if dark_history_24h >= 5:
|
||||
score += 10
|
||||
elif dark_history_24h >= 2:
|
||||
score += 5
|
||||
|
||||
score = min(score, 100)
|
||||
|
||||
|
||||
27
prediction/algorithms/vessel_type_mapping.py
Normal file
27
prediction/algorithms/vessel_type_mapping.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""한중어업협정 fishery_code → VesselType 매핑.
|
||||
|
||||
파이프라인 미통과 선박(경량 분석 경로)은 AIS 샘플 부족으로 분류기가 UNKNOWN 을 반환한다.
|
||||
등록선은 fishery_code 가 이미 확정이므로 이를 활용해 vessel_type 을 채운다.
|
||||
|
||||
VesselType 값 확장:
|
||||
기존: TRAWL / PURSE / LONGLINE / TRAP / UNKNOWN
|
||||
신규: GILLNET (유자망) / CARGO (운반선)
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
|
||||
FISHERY_CODE_TO_VESSEL_TYPE = {
|
||||
'PT': 'TRAWL', # 쌍끌이 저인망
|
||||
'PT-S': 'TRAWL', # 쌍끌이 부속선
|
||||
'OT': 'TRAWL', # 단선 저인망
|
||||
'GN': 'GILLNET', # 유자망
|
||||
'PS': 'PURSE', # 대형선망/위망
|
||||
'FC': 'CARGO', # 운반선 (조업 아님)
|
||||
}
|
||||
|
||||
|
||||
def fishery_code_to_vessel_type(fishery_code: Optional[str]) -> str:
|
||||
"""등록 어업 코드 → 선박 유형. 매칭 없으면 'UNKNOWN'."""
|
||||
if not fishery_code:
|
||||
return 'UNKNOWN'
|
||||
return FISHERY_CODE_TO_VESSEL_TYPE.get(fishery_code.upper(), 'UNKNOWN')
|
||||
@ -507,6 +507,7 @@ def run_analysis_cycle():
|
||||
# ── 5.5 경량 분석 — 파이프라인 미통과 412* 선박 ──
|
||||
# vessel_store._tracks의 24h 누적 궤적을 직접 활용하여 dark/spoof 신호도 산출.
|
||||
from algorithms.risk import compute_lightweight_risk_score
|
||||
from algorithms.vessel_type_mapping import fishery_code_to_vessel_type
|
||||
|
||||
pipeline_mmsis = {c['mmsi'] for c in classifications}
|
||||
lightweight_mmsis = vessel_store.get_chinese_mmsis() - pipeline_mmsis
|
||||
@ -607,17 +608,29 @@ def run_analysis_cycle():
|
||||
if spoof_score > 0.5:
|
||||
lw_spoof += 1
|
||||
|
||||
# dark_features 에 저장된 패턴 기반 점수 + 반복 이력을 리스크 산출에 직접 연결
|
||||
# (경량 경로가 45점 포화되던 원인 해소)
|
||||
risk_score, risk_level = compute_lightweight_risk_score(
|
||||
zone_info, sog, is_permitted=is_permitted,
|
||||
is_dark=dark, gap_duration_min=gap_min,
|
||||
spoofing_score=spoof_score,
|
||||
dark_suspicion_score=int(dark_features.get('dark_suspicion_score', 0) or 0),
|
||||
dist_from_baseline_nm=float(zone_info.get('dist_from_baseline_nm', 999.0) or 999.0),
|
||||
dark_history_24h=int(dark_features.get('dark_history_24h', 0) or 0),
|
||||
)
|
||||
|
||||
# 등록선은 fishery_code 로 vessel_type 채움 (미등록선은 UNKNOWN 유지)
|
||||
registered_fc = (
|
||||
fleet_tracker.get_registered_fishery_code(mmsi)
|
||||
if hasattr(fleet_tracker, 'get_registered_fishery_code') else None
|
||||
)
|
||||
vessel_type = fishery_code_to_vessel_type(registered_fc)
|
||||
|
||||
# BD-09 오프셋은 중국 선박이므로 제외 (412* = 중국)
|
||||
results.append(AnalysisResult(
|
||||
mmsi=mmsi,
|
||||
timestamp=ts,
|
||||
vessel_type='UNKNOWN',
|
||||
vessel_type=vessel_type,
|
||||
confidence=0.0,
|
||||
fishing_pct=0.0,
|
||||
lat=float(lat) if lat is not None else None,
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user