V029 fishery_permit_cn 스키마를 입력으로 하여 보류 중이던 G-02/G-03 판정 함수를 신설. classify_gear_violations() 시그니처에 permit_periods, registered_fishery_code, observation_ts 매개변수 추가. - G-02 (CLOSED_SEASON_FISHING, score 18): 관측 시각이 fishing_period_1/2 허가 기간 밖이면 금어기 조업 - G-03 (UNREGISTERED_GEAR, score 12): 감지 어구가 fishery_code 허용 어구 집합(PT→TRAWL/PT-S, GN→GILLNET, PS→PURSE, OT→TRAWL, FC→금지)에 없음 - fleet_tracker: _parse_period_range() 'YYYY/MM/DD - YYYY/MM/DD' 파서 + get_permit_periods() + get_registered_fishery_code() - violation_classifier: CLOSED_SEASON_FISHING / UNREGISTERED_GEAR judgment → ILLEGAL_GEAR 카테고리 매핑 데이터 부재(permit_periods 빈 값, fishery_code 미등록) 시 판정 보류 → False. 검증 목표: 1시간 내 G-02/G-03 각 ≥ 1건
108 lines
3.8 KiB
Python
108 lines
3.8 KiB
Python
"""
|
|
위반 유형 라벨링 — 분석 결과에 violation_categories[] 태깅.
|
|
|
|
vessel_analysis_results의 각 행에 대해 5개 위반 카테고리를 판정하고
|
|
violation_categories TEXT[] 컬럼을 업데이트합니다.
|
|
"""
|
|
import logging
|
|
from psycopg2.extras import execute_batch
|
|
|
|
from config import qualified_table
|
|
from db.kcgdb import get_conn
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
VAR_TABLE = qualified_table('vessel_analysis_results')
|
|
|
|
|
|
def classify_violations(result: dict) -> list[str]:
|
|
"""단일 분석 결과에 대해 위반 유형 리스트 반환.
|
|
|
|
판정 기준:
|
|
- EEZ_VIOLATION: 중국선박(412*) + EEZ/NLL/특별금어구역 + 비허가
|
|
- DARK_VESSEL: is_dark + 30분 이상 갭
|
|
- MMSI_TAMPERING: spoofing_score > 0.6
|
|
- ILLEGAL_TRANSSHIP: transship_suspect
|
|
- RISK_BEHAVIOR: 위반 없이 risk_score >= 70
|
|
"""
|
|
violations = []
|
|
|
|
zone = result.get('zone_code', '') or ''
|
|
risk_score = result.get('risk_score', 0) or 0
|
|
is_dark = result.get('is_dark', False)
|
|
spoofing = result.get('spoofing_score', 0) or 0
|
|
transship = result.get('transship_suspect', False)
|
|
gap_min = result.get('gap_duration_min', 0) or 0
|
|
mmsi = str(result.get('mmsi', '') or '')
|
|
# permit_status는 선택적 — 없으면 중국 선박인지로 판단 (412* prefix)
|
|
permit = result.get('permit_status') or ''
|
|
is_chinese = mmsi.startswith('412') or mmsi.startswith('413')
|
|
|
|
# EEZ 침범: 중국선박이 한국 해역에 진입 (중국선박은 기본적으로 비허가 상정)
|
|
if zone in ('NLL', 'SPECIAL_FISHING_1', 'SPECIAL_FISHING_2',
|
|
'SPECIAL_FISHING_3', 'SPECIAL_FISHING_4', 'EEZ_KR'):
|
|
if is_chinese and permit not in ('VALID', 'PERMITTED'):
|
|
violations.append('EEZ_VIOLATION')
|
|
elif permit in ('NONE', 'EXPIRED', 'REVOKED'):
|
|
violations.append('EEZ_VIOLATION')
|
|
|
|
# 다크베셀
|
|
if is_dark and gap_min > 30:
|
|
violations.append('DARK_VESSEL')
|
|
|
|
# MMSI 변조
|
|
if spoofing > 0.6:
|
|
violations.append('MMSI_TAMPERING')
|
|
|
|
# 불법환적
|
|
if transship:
|
|
violations.append('ILLEGAL_TRANSSHIP')
|
|
|
|
# 어구 불법 (gear_judgment은 classify_gear_violations()로 채워짐: G-01~G-06)
|
|
gear_judgment = result.get('gear_judgment', '') or ''
|
|
if gear_judgment in (
|
|
'NO_PERMIT', 'GEAR_MISMATCH', 'ZONE_VIOLATION', 'SEASON_VIOLATION',
|
|
'PAIR_TRAWL', 'CLOSED_SEASON_FISHING', 'UNREGISTERED_GEAR',
|
|
):
|
|
violations.append('ILLEGAL_GEAR')
|
|
|
|
# 위험 행동 (다른 위반 없이 고위험)
|
|
if not violations and risk_score >= 70:
|
|
violations.append('RISK_BEHAVIOR')
|
|
|
|
return violations
|
|
|
|
|
|
def run_violation_classifier(analysis_results: list[dict]) -> dict:
|
|
"""
|
|
분석 결과 리스트에 위반 카테고리를 라벨링하고 DB 업데이트.
|
|
|
|
AnalysisResult에는 DB id가 없으므로 (mmsi, analyzed_at)으로 UPDATE.
|
|
|
|
Returns:
|
|
{ 'classified': int, 'violations_found': int }
|
|
"""
|
|
updates = []
|
|
violations_found = 0
|
|
|
|
for result in analysis_results:
|
|
violations = classify_violations(result)
|
|
mmsi = result.get('mmsi')
|
|
analyzed_at = result.get('analyzed_at')
|
|
if mmsi and analyzed_at and violations:
|
|
updates.append((violations, str(mmsi), analyzed_at))
|
|
violations_found += len(violations)
|
|
|
|
if updates:
|
|
with get_conn() as conn:
|
|
execute_batch(
|
|
conn.cursor(),
|
|
f"UPDATE {VAR_TABLE} SET violation_categories = %s "
|
|
f"WHERE mmsi = %s AND analyzed_at = %s",
|
|
updates,
|
|
)
|
|
conn.commit()
|
|
|
|
logger.info(f'violation_classifier: classified={len(updates)}, violations={violations_found}')
|
|
return {'classified': len(updates), 'violations_found': violations_found}
|