diff --git a/backend/src/main/resources/db/migration/V014__fleet_prediction_tables.sql b/backend/src/main/resources/db/migration/V014__fleet_prediction_tables.sql new file mode 100644 index 0000000..98c2dde --- /dev/null +++ b/backend/src/main/resources/db/migration/V014__fleet_prediction_tables.sql @@ -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; diff --git a/backend/src/main/resources/db/migration/V015__fix_numeric_precision.sql b/backend/src/main/resources/db/migration/V015__fix_numeric_precision.sql new file mode 100644 index 0000000..aecaa97 --- /dev/null +++ b/backend/src/main/resources/db/migration/V015__fix_numeric_precision.sql @@ -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); diff --git a/prediction/db/kcgdb.py b/prediction/db/kcgdb.py index 6654744..8d61838 100644 --- a/prediction/db/kcgdb.py +++ b/prediction/db/kcgdb.py @@ -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: diff --git a/prediction/models/result.py b/prediction/models/result.py index 3ef41a1..bb7a69c 100644 --- a/prediction/models/result.py +++ b/prediction/models/result.py @@ -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, )