fix: prediction e2e — 누락 테이블 12개 + 컬럼 매핑 + NUMERIC precision 통합 수정

- V014: fleet_vessels, fleet_tracking_snapshot, gear_identity_log,
  gear_correlation_scores/raw_metrics, correlation_param_models,
  group_polygon_snapshots, gear_group_episodes/episode_snapshots,
  gear_group_parent_candidate_snapshots, gear_parent_label_tracking_cycles,
  system_config 테이블 추가
- V015: 점수/비율 NUMERIC precision 일괄 확대 (score→7,4 / pct→12,2) +
  vessel_analysis_results UNIQUE(mmsi, analyzed_at) 인덱스 추가
- prediction kcgdb.py: timestamp→analyzed_at, zone→zone_code,
  is_leader→fleet_is_leader, is_transship_suspect→transship_suspect 매핑

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
htlee 2026-04-07 14:49:10 +09:00
부모 4db7874082
커밋 e12d1c33e2
4개의 변경된 파일379개의 추가작업 그리고 18개의 파일을 삭제

파일 보기

@ -0,0 +1,283 @@
-- V014: prediction에서 참조하는 누락 테이블 추가
-- fleet_vessels, fleet_tracking_snapshot, gear_identity_log,
-- gear_correlation_scores, gear_correlation_raw_metrics, correlation_param_models,
-- group_polygon_snapshots, gear_group_episodes, gear_group_episode_snapshots,
-- gear_group_parent_candidate_snapshots, gear_parent_label_tracking_cycles, system_config
-- ===== 1. fleet_vessels =====
CREATE TABLE IF NOT EXISTS kcg.fleet_vessels (
id BIGSERIAL PRIMARY KEY,
company_id BIGINT NOT NULL REFERENCES kcg.fleet_companies(id),
permit_no VARCHAR(50),
name_cn VARCHAR(100),
name_en VARCHAR(100),
tonnage NUMERIC(10,2),
gear_code VARCHAR(20),
fleet_role VARCHAR(20) DEFAULT 'CREW', -- MAIN, CREW, TRANSPORT, NOISE
pair_vessel_id BIGINT,
mmsi VARCHAR(20),
match_confidence NUMERIC(4,3),
match_method VARCHAR(30), -- NAME_EXACT, NAME_PARENT
last_seen_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_fleet_vessels_company ON kcg.fleet_vessels(company_id);
CREATE INDEX idx_fleet_vessels_mmsi ON kcg.fleet_vessels(mmsi);
CREATE INDEX idx_fleet_vessels_name_cn ON kcg.fleet_vessels(name_cn);
-- ===== 2. fleet_tracking_snapshot =====
CREATE TABLE IF NOT EXISTS kcg.fleet_tracking_snapshot (
id BIGSERIAL PRIMARY KEY,
company_id BIGINT NOT NULL REFERENCES kcg.fleet_companies(id),
snapshot_time TIMESTAMPTZ NOT NULL,
total_vessels INT NOT NULL DEFAULT 0,
active_vessels INT NOT NULL DEFAULT 0,
center_lat NUMERIC(9,6),
center_lon NUMERIC(10,6)
);
CREATE INDEX idx_fleet_snapshot_company ON kcg.fleet_tracking_snapshot(company_id, snapshot_time DESC);
-- ===== 3. gear_identity_log =====
CREATE TABLE IF NOT EXISTS kcg.gear_identity_log (
id BIGSERIAL PRIMARY KEY,
mmsi VARCHAR(20) NOT NULL,
name VARCHAR(200) NOT NULL,
parent_name VARCHAR(100),
parent_mmsi VARCHAR(20),
parent_vessel_id BIGINT,
gear_index_1 INT,
gear_index_2 INT,
lat NUMERIC(9,6),
lon NUMERIC(10,6),
match_method VARCHAR(30),
match_confidence NUMERIC(4,3),
first_seen_at TIMESTAMPTZ NOT NULL,
last_seen_at TIMESTAMPTZ NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE INDEX idx_gear_identity_mmsi ON kcg.gear_identity_log(mmsi, is_active);
CREATE INDEX idx_gear_identity_name ON kcg.gear_identity_log(name, is_active);
-- ===== 4. correlation_param_models =====
CREATE TABLE IF NOT EXISTS kcg.correlation_param_models (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
params JSONB NOT NULL DEFAULT '{}',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 기본 모델 시드
INSERT INTO kcg.correlation_param_models (name, params, is_active, is_default) VALUES
('default', '{"ema_alpha": 0.3, "proximity_weight": 0.25, "visit_weight": 0.2, "activity_sync_weight": 0.15, "heading_weight": 0.15, "dtw_weight": 0.1, "speed_weight": 0.1, "drift_weight": 0.05}', TRUE, TRUE)
ON CONFLICT DO NOTHING;
-- ===== 5. gear_correlation_scores =====
CREATE TABLE IF NOT EXISTS kcg.gear_correlation_scores (
model_id BIGINT NOT NULL REFERENCES kcg.correlation_param_models(id),
group_key VARCHAR(255) NOT NULL,
sub_cluster_id INT NOT NULL,
target_mmsi VARCHAR(20) NOT NULL,
target_type VARCHAR(20), -- VESSEL, GEAR_BUOY
target_name VARCHAR(200),
current_score NUMERIC(6,4) DEFAULT 0,
proximity_ratio NUMERIC(6,4),
visit_score NUMERIC(6,4),
heading_coherence NUMERIC(6,4),
streak_count INT DEFAULT 0,
freeze_state VARCHAR(20) DEFAULT 'ACTIVE', -- ACTIVE, FREEZE, SIGNAL_LOSS
observation_count INT DEFAULT 0,
first_observed_at TIMESTAMPTZ,
last_observed_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (model_id, group_key, sub_cluster_id, target_mmsi)
);
CREATE INDEX idx_corr_scores_group ON kcg.gear_correlation_scores(group_key, sub_cluster_id);
CREATE INDEX idx_corr_scores_target ON kcg.gear_correlation_scores(target_mmsi);
-- ===== 6. gear_correlation_raw_metrics =====
CREATE TABLE IF NOT EXISTS kcg.gear_correlation_raw_metrics (
id BIGSERIAL PRIMARY KEY,
observed_at TIMESTAMPTZ NOT NULL,
group_key VARCHAR(255) NOT NULL,
sub_cluster_id INT NOT NULL,
target_mmsi VARCHAR(20) NOT NULL,
target_type VARCHAR(20),
target_name VARCHAR(200),
proximity_ratio NUMERIC(6,4),
visit_score NUMERIC(6,4),
activity_sync NUMERIC(6,4),
dtw_similarity NUMERIC(6,4),
speed_correlation NUMERIC(6,4),
heading_coherence NUMERIC(6,4),
drift_similarity NUMERIC(6,4),
shadow_stay BOOLEAN DEFAULT FALSE,
shadow_return BOOLEAN DEFAULT FALSE,
gear_group_active_ratio NUMERIC(6,4)
);
CREATE INDEX idx_raw_metrics_observed ON kcg.gear_correlation_raw_metrics(observed_at);
CREATE INDEX idx_raw_metrics_group ON kcg.gear_correlation_raw_metrics(group_key, sub_cluster_id, observed_at);
-- ===== 7. group_polygon_snapshots =====
CREATE TABLE IF NOT EXISTS kcg.group_polygon_snapshots (
id BIGSERIAL PRIMARY KEY,
group_type VARCHAR(30) NOT NULL, -- FLEET, GEAR_IN_ZONE, GEAR_OUT_ZONE
group_key VARCHAR(255) NOT NULL,
group_label VARCHAR(255),
sub_cluster_id INT NOT NULL DEFAULT 0,
resolution VARCHAR(10) DEFAULT '6h', -- 6h, 1h
snapshot_time TIMESTAMPTZ NOT NULL,
polygon GEOMETRY(Polygon, 4326),
center_point GEOMETRY(Point, 4326),
area_sq_nm NUMERIC(12,4),
member_count INT DEFAULT 0,
zone_id VARCHAR(50),
zone_name VARCHAR(100),
members JSONB,
color VARCHAR(20)
);
CREATE INDEX idx_polygon_snap_group ON kcg.group_polygon_snapshots(group_key, sub_cluster_id, snapshot_time DESC);
CREATE INDEX idx_polygon_snap_time ON kcg.group_polygon_snapshots(snapshot_time);
CREATE INDEX idx_polygon_snap_geom ON kcg.group_polygon_snapshots USING GIST (polygon);
-- ===== 8. gear_group_episodes =====
CREATE TABLE IF NOT EXISTS kcg.gear_group_episodes (
episode_id VARCHAR(50) PRIMARY KEY, -- 'ep-{12hex}'
lineage_key VARCHAR(255) NOT NULL,
group_key VARCHAR(255) NOT NULL,
normalized_parent_name VARCHAR(100),
current_sub_cluster_id INT,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE, EXPIRED, MERGED
continuity_source VARCHAR(30), -- NEW, CONTINUED, SPLIT_NEW, SPLIT_CONTINUE, MERGE_NEW
continuity_score NUMERIC(6,4),
first_seen_at TIMESTAMPTZ,
last_seen_at TIMESTAMPTZ,
last_snapshot_time TIMESTAMPTZ,
current_member_count INT DEFAULT 0,
current_member_mmsis JSONB,
current_center_point GEOMETRY(Point, 4326),
split_from_episode_id VARCHAR(50),
merged_from_episode_ids JSONB,
merged_into_episode_id VARCHAR(50),
metadata JSONB,
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_episodes_lineage ON kcg.gear_group_episodes(lineage_key, status);
CREATE INDEX idx_episodes_group ON kcg.gear_group_episodes(group_key, status);
CREATE INDEX idx_episodes_snapshot ON kcg.gear_group_episodes(last_snapshot_time);
-- ===== 9. gear_group_episode_snapshots =====
CREATE TABLE IF NOT EXISTS kcg.gear_group_episode_snapshots (
episode_id VARCHAR(50) NOT NULL,
lineage_key VARCHAR(255),
group_key VARCHAR(255) NOT NULL,
normalized_parent_name VARCHAR(100),
sub_cluster_id INT NOT NULL,
observed_at TIMESTAMPTZ NOT NULL,
member_count INT DEFAULT 0,
member_mmsis JSONB,
center_point GEOMETRY(Point, 4326),
continuity_source VARCHAR(30),
continuity_score NUMERIC(6,4),
parent_episode_ids JSONB,
top_candidate_mmsi VARCHAR(20),
top_candidate_score NUMERIC(6,4),
resolution_status VARCHAR(30),
metadata JSONB,
PRIMARY KEY (episode_id, observed_at)
);
CREATE INDEX idx_ep_snap_group ON kcg.gear_group_episode_snapshots(group_key, sub_cluster_id, observed_at DESC);
-- ===== 10. gear_group_parent_candidate_snapshots =====
CREATE TABLE IF NOT EXISTS kcg.gear_group_parent_candidate_snapshots (
id BIGSERIAL PRIMARY KEY,
observed_at TIMESTAMPTZ NOT NULL,
group_key VARCHAR(255) NOT NULL,
sub_cluster_id INT NOT NULL,
parent_name VARCHAR(100),
normalized_parent_name VARCHAR(100),
episode_id VARCHAR(50),
candidate_mmsi VARCHAR(20) NOT NULL,
candidate_name VARCHAR(200),
candidate_vessel_id BIGINT,
rank INT,
candidate_source VARCHAR(30),
model_id BIGINT,
model_name VARCHAR(100),
base_corr_score NUMERIC(6,4),
name_match_score NUMERIC(6,4),
track_similarity_score NUMERIC(6,4),
visit_score_6h NUMERIC(6,4),
proximity_score_6h NUMERIC(6,4),
activity_sync_score_6h NUMERIC(6,4),
stability_score NUMERIC(6,4),
registry_bonus NUMERIC(6,4),
episode_prior_bonus NUMERIC(6,4),
lineage_prior_bonus NUMERIC(6,4),
label_prior_bonus NUMERIC(6,4),
final_score NUMERIC(6,4),
margin_from_top NUMERIC(6,4),
evidence JSONB
);
CREATE INDEX idx_candidate_snap_group ON kcg.gear_group_parent_candidate_snapshots(group_key, sub_cluster_id, observed_at DESC);
CREATE INDEX idx_candidate_snap_episode ON kcg.gear_group_parent_candidate_snapshots(episode_id, observed_at DESC);
CREATE INDEX idx_candidate_snap_norm ON kcg.gear_group_parent_candidate_snapshots(normalized_parent_name);
-- ===== 11. gear_parent_label_tracking_cycles =====
CREATE TABLE IF NOT EXISTS kcg.gear_parent_label_tracking_cycles (
label_session_id BIGINT NOT NULL REFERENCES kcg.gear_parent_label_sessions(id),
observed_at TIMESTAMPTZ NOT NULL,
candidate_snapshot_observed_at TIMESTAMPTZ,
auto_status VARCHAR(30),
top_candidate_mmsi VARCHAR(20),
top_candidate_name VARCHAR(200),
top_candidate_score NUMERIC(6,4),
top_candidate_margin NUMERIC(6,4),
candidate_count INT,
labeled_candidate_present BOOLEAN,
labeled_candidate_rank INT,
labeled_candidate_score NUMERIC(6,4),
labeled_candidate_pre_bonus_score NUMERIC(6,4),
labeled_candidate_margin_from_top NUMERIC(6,4),
matched_top1 BOOLEAN,
matched_top3 BOOLEAN,
evidence_summary JSONB,
PRIMARY KEY (label_session_id, observed_at)
);
-- ===== 12. system_config =====
CREATE TABLE IF NOT EXISTS kcg.system_config (
key VARCHAR(100) PRIMARY KEY,
value JSONB,
description TEXT,
updated_at TIMESTAMPTZ DEFAULT now()
);
-- 파티션 기본값 시드
INSERT INTO kcg.system_config (key, value, description) VALUES
('partition.raw_metrics.retention_days', '7', '원시 메트릭 보관 일수'),
('partition.raw_metrics.create_ahead_days', '3', '파티션 사전 생성 일수'),
('partition.scores.cleanup_days', '30', '점수 정리 일수')
ON CONFLICT (key) DO NOTHING;
-- ===== fleet_vessels 시드 데이터 (fleet_companies 기반) =====
-- 중국 원양어업 회사 소속 선박 (데모용)
INSERT INTO kcg.fleet_vessels (company_id, permit_no, name_cn, name_en, tonnage, gear_code, fleet_role) VALUES
(1, 'ZY-2024-001', '鲁荣渔2682', 'LU RONG YU 2682', 450.0, 'TRAWL', 'MAIN'),
(1, 'ZY-2024-002', '鲁荣渔2683', 'LU RONG YU 2683', 380.0, 'TRAWL', 'CREW'),
(1, 'ZY-2024-003', '鲁荣渔2680', 'LU RONG YU 2680', 520.0, 'PURSE', 'MAIN'),
(2, 'ZY-2024-010', '浙岱渔11032', 'ZHE DAI YU 11032', 600.0, 'PURSE', 'MAIN'),
(2, 'ZY-2024-011', '浙岱渔11033', 'ZHE DAI YU 11033', 550.0, 'PURSE', 'CREW'),
(2, 'ZY-2024-012', '浙岱渔运108', 'ZHE DAI YU YUN 108', 800.0, 'TRANSPORT', 'TRANSPORT')
ON CONFLICT DO NOTHING;

파일 보기

@ -0,0 +1,80 @@
-- V015: 점수/비율 NUMERIC 컬럼 precision 일괄 확대 + vessel_analysis_results UNIQUE 제약
-- === 1. vessel_analysis_results 점수 컬럼 ===
ALTER TABLE kcg.vessel_analysis_results ALTER COLUMN confidence TYPE NUMERIC(7,4);
ALTER TABLE kcg.vessel_analysis_results ALTER COLUMN fishing_pct TYPE NUMERIC(7,4);
ALTER TABLE kcg.vessel_analysis_results ALTER COLUMN ucaf_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.vessel_analysis_results ALTER COLUMN ucft_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.vessel_analysis_results ALTER COLUMN spoofing_score TYPE NUMERIC(7,4);
-- ON CONFLICT용 UNIQUE 제약 (파티션 테이블은 PK가 있지만 ON CONFLICT 절에서 인식 안 됨)
-- 파티션 PK (id, analyzed_at)와 별도로 mmsi+analyzed_at UNIQUE 추가
CREATE UNIQUE INDEX IF NOT EXISTS idx_var_mmsi_analyzed ON kcg.vessel_analysis_results (mmsi, analyzed_at);
-- === 2. prediction_events/alerts ai_confidence ===
ALTER TABLE kcg.prediction_events ALTER COLUMN ai_confidence TYPE NUMERIC(7,4);
ALTER TABLE kcg.prediction_alerts ALTER COLUMN ai_confidence TYPE NUMERIC(7,4);
ALTER TABLE kcg.enforcement_records ALTER COLUMN ai_confidence TYPE NUMERIC(7,4);
ALTER TABLE kcg.prediction_label_input ALTER COLUMN confidence TYPE NUMERIC(7,4);
-- === 3. AI 모델 메트릭 ===
ALTER TABLE kcg.ai_model_versions ALTER COLUMN accuracy_pct TYPE NUMERIC(7,2);
ALTER TABLE kcg.ai_model_versions ALTER COLUMN precision_pct TYPE NUMERIC(7,2);
ALTER TABLE kcg.ai_model_versions ALTER COLUMN recall_pct TYPE NUMERIC(7,2);
ALTER TABLE kcg.ai_model_versions ALTER COLUMN f1_score TYPE NUMERIC(7,4);
-- === 4. gear 관련 score 컬럼 NUMERIC(6,4) → NUMERIC(7,4) ===
-- gear_correlation_scores
ALTER TABLE kcg.gear_correlation_scores ALTER COLUMN current_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_correlation_scores ALTER COLUMN proximity_ratio TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_correlation_scores ALTER COLUMN visit_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_correlation_scores ALTER COLUMN heading_coherence TYPE NUMERIC(7,4);
-- gear_correlation_raw_metrics
ALTER TABLE kcg.gear_correlation_raw_metrics ALTER COLUMN proximity_ratio TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_correlation_raw_metrics ALTER COLUMN visit_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_correlation_raw_metrics ALTER COLUMN activity_sync TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_correlation_raw_metrics ALTER COLUMN dtw_similarity TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_correlation_raw_metrics ALTER COLUMN speed_correlation TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_correlation_raw_metrics ALTER COLUMN heading_coherence TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_correlation_raw_metrics ALTER COLUMN drift_similarity TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_correlation_raw_metrics ALTER COLUMN gear_group_active_ratio TYPE NUMERIC(7,4);
-- gear_group_episodes
ALTER TABLE kcg.gear_group_episodes ALTER COLUMN continuity_score TYPE NUMERIC(7,4);
-- gear_group_episode_snapshots
ALTER TABLE kcg.gear_group_episode_snapshots ALTER COLUMN continuity_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_episode_snapshots ALTER COLUMN top_candidate_score TYPE NUMERIC(7,4);
-- gear_group_parent_candidate_snapshots (13개 score 컬럼)
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN base_corr_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN name_match_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN track_similarity_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN visit_score_6h TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN proximity_score_6h TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN activity_sync_score_6h TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN stability_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN registry_bonus TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN episode_prior_bonus TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN lineage_prior_bonus TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN label_prior_bonus TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN final_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_group_parent_candidate_snapshots ALTER COLUMN margin_from_top TYPE NUMERIC(7,4);
-- gear_parent_label_tracking_cycles
ALTER TABLE kcg.gear_parent_label_tracking_cycles ALTER COLUMN top_candidate_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_parent_label_tracking_cycles ALTER COLUMN top_candidate_margin TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_parent_label_tracking_cycles ALTER COLUMN labeled_candidate_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_parent_label_tracking_cycles ALTER COLUMN labeled_candidate_pre_bonus_score TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_parent_label_tracking_cycles ALTER COLUMN labeled_candidate_margin_from_top TYPE NUMERIC(7,4);
-- === 5. match_confidence NUMERIC(4,3) → NUMERIC(7,4) ===
ALTER TABLE kcg.fleet_vessels ALTER COLUMN match_confidence TYPE NUMERIC(7,4);
ALTER TABLE kcg.gear_identity_log ALTER COLUMN match_confidence TYPE NUMERIC(7,4);
-- === 6. stats/kpi 테이블 ===
ALTER TABLE kcg.prediction_stats_daily ALTER COLUMN ai_accuracy_pct TYPE NUMERIC(12,2);
ALTER TABLE kcg.prediction_stats_monthly ALTER COLUMN ai_accuracy_pct TYPE NUMERIC(12,2);
ALTER TABLE kcg.prediction_kpi_realtime ALTER COLUMN delta_pct TYPE NUMERIC(12,2);
ALTER TABLE kcg.prediction_risk_grid ALTER COLUMN avg_risk TYPE NUMERIC(12,2);

파일 보기

@ -71,22 +71,22 @@ def upsert_results(results: list['AnalysisResult']) -> int:
insert_sql = """
INSERT INTO vessel_analysis_results (
mmsi, timestamp, vessel_type, confidence, fishing_pct,
cluster_id, season, zone, dist_to_baseline_nm, activity_state,
mmsi, analyzed_at, vessel_type, confidence, fishing_pct,
cluster_id, season, zone_code, dist_to_baseline_nm, activity_state,
ucaf_score, ucft_score, is_dark, gap_duration_min,
spoofing_score, bd09_offset_m, speed_jump_count,
cluster_size, is_leader, fleet_role,
fleet_cluster_id, fleet_is_leader, fleet_role,
risk_score, risk_level,
is_transship_suspect, transship_pair_mmsi, transship_duration_min,
features, analyzed_at
transship_suspect, transship_pair_mmsi, transship_duration_min,
features
) VALUES %s
ON CONFLICT (mmsi, timestamp) DO UPDATE SET
ON CONFLICT (mmsi, analyzed_at) DO UPDATE SET
vessel_type = EXCLUDED.vessel_type,
confidence = EXCLUDED.confidence,
fishing_pct = EXCLUDED.fishing_pct,
cluster_id = EXCLUDED.cluster_id,
season = EXCLUDED.season,
zone = EXCLUDED.zone,
zone_code = EXCLUDED.zone_code,
dist_to_baseline_nm = EXCLUDED.dist_to_baseline_nm,
activity_state = EXCLUDED.activity_state,
ucaf_score = EXCLUDED.ucaf_score,
@ -96,16 +96,15 @@ def upsert_results(results: list['AnalysisResult']) -> int:
spoofing_score = EXCLUDED.spoofing_score,
bd09_offset_m = EXCLUDED.bd09_offset_m,
speed_jump_count = EXCLUDED.speed_jump_count,
cluster_size = EXCLUDED.cluster_size,
is_leader = EXCLUDED.is_leader,
fleet_cluster_id = EXCLUDED.fleet_cluster_id,
fleet_is_leader = EXCLUDED.fleet_is_leader,
fleet_role = EXCLUDED.fleet_role,
risk_score = EXCLUDED.risk_score,
risk_level = EXCLUDED.risk_level,
is_transship_suspect = EXCLUDED.is_transship_suspect,
transship_suspect = EXCLUDED.transship_suspect,
transship_pair_mmsi = EXCLUDED.transship_pair_mmsi,
transship_duration_min = EXCLUDED.transship_duration_min,
features = EXCLUDED.features,
analyzed_at = EXCLUDED.analyzed_at
features = EXCLUDED.features
"""
try:

파일 보기

@ -75,13 +75,13 @@ class AnalysisResult:
return (
str(self.mmsi),
self.timestamp,
self.analyzed_at, # analyzed_at (PK 파티션키)
str(self.vessel_type),
_f(self.confidence),
_f(self.fishing_pct),
_i(self.cluster_id),
str(self.season),
str(self.zone),
str(self.zone), # → zone_code
_f(self.dist_to_baseline_nm),
str(self.activity_state),
_f(self.ucaf_score),
@ -91,14 +91,13 @@ class AnalysisResult:
_f(self.spoofing_score),
_f(self.bd09_offset_m),
_i(self.speed_jump_count),
_i(self.cluster_size),
bool(self.is_leader),
_i(self.cluster_id), # → fleet_cluster_id
bool(self.is_leader), # → fleet_is_leader
str(self.fleet_role),
_i(self.risk_score),
str(self.risk_level),
bool(self.is_transship_suspect),
bool(self.is_transship_suspect), # → transship_suspect
str(self.transship_pair_mmsi),
_i(self.transship_duration_min),
json.dumps(safe_features),
self.analyzed_at,
)