## 이슈 1: gear_correlation Decimal → float TypeError
- prediction/algorithms/gear_correlation.py:785
- _load_all_scores()가 NUMERIC 컬럼을 Decimal로 읽어 float 상수와 연산 시 실패
- float() 명시 변환으로 수정
- 효과: gear correlation 24,474 raw metrics + 3,966 scores 정상 기록
## 이슈 2: violation_classifier classified=0 문제
- prediction/output/violation_classifier.py
- result.get('id')는 AnalysisResult에 없어 항상 None → 모든 UPDATE 건너뜀
- 존재하지 않는 permit_status/gear_judgment 필드에 의존
- (mmsi, analyzed_at) 기준 UPDATE로 변경
- 중국 선박(412/413*) + EEZ 진입은 permit 없어도 EEZ_VIOLATION 판정
- 효과: classified=0 → classified=4~6/cycle
## 이슈 3: kpi_writer 모두 0 (tracking_active 외)
- prediction/output/kpi_writer.py:27
- date.today() + timezone.utc 혼용 → 현재 시각이 UTC로는 아직 '어제'라 '오늘 >= today_start' 쿼리가 0 반환
- KST 기준으로 today_start 계산
- 효과: realtime_detection 0 → 7,107, illegal_transship 0 → 5,033
## 이슈 4: stats_daily 오늘 0건
- prediction/output/stats_aggregator.py:96, 194
- aggregate_daily/monthly가 UTC 경계 사용
- KST 기준 자정으로 수정
- 효과: 2026-04-08 detections 0 → 543,656, events 0 → 5,253
## 이슈 5: parent workflow 테이블 누락 컬럼 (V005 ↔ prediction 불일치)
V016 마이그레이션으로 일괄 추가:
- gear_parent_label_sessions: label_parent_name, normalized_parent_name,
duration_days, actor, comment, metadata, updated_at 등 8개 컬럼
- gear_group_parent_resolution: parent_name, normalized_parent_name,
selected_parent_name, confidence, decision_source, top_score, second_score,
score_margin, stable_cycles, evidence_summary, episode_id, continuity_*,
prior_bonus_total, last_evaluated_at, last_promoted_at 등 17개 컬럼
- gear_parent_candidate_exclusions: normalized_parent_name, reason_type,
duration_days, metadata, updated_at, active_from, active_until +
candidate_mmsi GENERATED ALWAYS AS (excluded_mmsi) 별칭
- gear_group_parent_candidate_snapshots: parent_name
효과: gear parent inference: 925 groups, 301 direct-match, 1329 candidates,
188 review-required, 925 episode-snapshots 기록 — 전체 모선 워크플로우 정상
## 검증 결과 (e2e)
- analysis cycle: 6,824 vessels, 112초/cycle 정상
- vessel_analysis_results: 10분 13,650건, 총 125만건
- prediction_events: 1시간 138건, 총 12,258건
- prediction_alerts: 1시간 183건
- gear_correlation_scores: 3,966건
- gear_group_parent_resolution: 926건
- stats_hourly: 17행, stats_daily: 오늘 543,656건
- 백엔드 Flyway V016 정상 적용
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
114 lines
3.8 KiB
Python
114 lines
3.8 KiB
Python
"""
|
|
실시간 KPI 갱신 — prediction_kpi_realtime 테이블 업데이트.
|
|
|
|
매 분석 사이클마다 오늘 날짜 기준 카운트를 계산하여 6개 KPI 갱신.
|
|
"""
|
|
import logging
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from config import qualified_table
|
|
from db.kcgdb import get_conn
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
KPI_TABLE = qualified_table('prediction_kpi_realtime')
|
|
EVENTS_TABLE = qualified_table('prediction_events')
|
|
ENF_TABLE = qualified_table('enforcement_records')
|
|
VAR_TABLE = qualified_table('vessel_analysis_results')
|
|
|
|
# 한국 표준시 (운영 기준)
|
|
_KST = timezone(timedelta(hours=9))
|
|
|
|
|
|
def run_kpi_writer() -> dict:
|
|
"""
|
|
오늘(KST) 날짜 기준으로 6개 KPI를 재계산하여 갱신.
|
|
|
|
Returns:
|
|
{ kpi_key: value } 딕셔너리
|
|
"""
|
|
# KST 기준 "오늘" 시작 시각 (해당 시각은 UTC로도 비교 가능하므로 DB 필드가 TIMESTAMPTZ면 안전)
|
|
now_kst = datetime.now(_KST)
|
|
today_start = now_kst.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
now = datetime.now(timezone.utc)
|
|
results = {}
|
|
|
|
with get_conn() as conn:
|
|
cur = conn.cursor()
|
|
|
|
# 1. 실시간 탐지 (오늘 분석 결과 수)
|
|
cur.execute(
|
|
f"SELECT COUNT(DISTINCT mmsi) FROM {VAR_TABLE} WHERE analyzed_at >= %s",
|
|
(today_start,)
|
|
)
|
|
realtime = cur.fetchone()[0] or 0
|
|
results['realtime_detection'] = realtime
|
|
|
|
# 2. EEZ 침범 (오늘 EEZ 관련 이벤트)
|
|
cur.execute(
|
|
f"SELECT COUNT(*) FROM {EVENTS_TABLE} WHERE category = 'EEZ_INTRUSION' AND occurred_at >= %s",
|
|
(today_start,)
|
|
)
|
|
eez = cur.fetchone()[0] or 0
|
|
results['eez_violation'] = eez
|
|
|
|
# 3. 다크베셀 (현재 dark 상태인 선박)
|
|
cur.execute(
|
|
f"""SELECT COUNT(DISTINCT mmsi) FROM {VAR_TABLE}
|
|
WHERE is_dark = true AND analyzed_at >= %s""",
|
|
(today_start,)
|
|
)
|
|
dark = cur.fetchone()[0] or 0
|
|
results['dark_vessel'] = dark
|
|
|
|
# 4. 환적 의심 (오늘)
|
|
cur.execute(
|
|
f"""SELECT COUNT(*) FROM {EVENTS_TABLE}
|
|
WHERE category = 'ILLEGAL_TRANSSHIP' AND occurred_at >= %s""",
|
|
(today_start,)
|
|
)
|
|
transship = cur.fetchone()[0] or 0
|
|
results['illegal_transship'] = transship
|
|
|
|
# 5. 추적 중 (IN_PROGRESS 상태 이벤트)
|
|
cur.execute(
|
|
f"SELECT COUNT(*) FROM {EVENTS_TABLE} WHERE status = 'IN_PROGRESS'"
|
|
)
|
|
tracking = cur.fetchone()[0] or 0
|
|
results['tracking_active'] = tracking
|
|
|
|
# 6. 나포/검문 (오늘 단속)
|
|
cur.execute(
|
|
f"SELECT COUNT(*) FROM {ENF_TABLE} WHERE enforced_at >= %s",
|
|
(today_start,)
|
|
)
|
|
captured = cur.fetchone()[0] or 0
|
|
results['captured_inspected'] = captured
|
|
|
|
# KPI 테이블 업데이트 (이전 값과 비교하여 trend 계산)
|
|
for key, value in results.items():
|
|
cur.execute(
|
|
f"SELECT value FROM {KPI_TABLE} WHERE kpi_key = %s",
|
|
(key,)
|
|
)
|
|
row = cur.fetchone()
|
|
prev = row[0] if row else 0
|
|
if value > prev:
|
|
trend, delta = 'up', ((value - prev) / max(prev, 1)) * 100
|
|
elif value < prev:
|
|
trend, delta = 'down', ((value - prev) / max(prev, 1)) * 100
|
|
else:
|
|
trend, delta = 'flat', 0.0
|
|
|
|
cur.execute(
|
|
f"""UPDATE {KPI_TABLE}
|
|
SET value = %s, trend = %s, delta_pct = %s, updated_at = %s
|
|
WHERE kpi_key = %s""",
|
|
(value, trend, round(delta, 2), now, key)
|
|
)
|
|
|
|
conn.commit()
|
|
|
|
logger.info(f'kpi_writer: {results}')
|
|
return results
|