From 4c22d5f1f9024a71f45418009dfeec5720b0e878 Mon Sep 17 00:00:00 2001 From: htlee Date: Tue, 24 Mar 2026 14:17:44 +0900 Subject: [PATCH] =?UTF-8?q?fix(prediction):=20=EC=96=B4=EA=B5=AC=EA=B7=B8?= =?UTF-8?q?=EB=A3=B9=20=ED=83=90=EC=A7=80=20=E2=80=94=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20AIS=20=EC=84=A0=EB=B0=95=20=EB=8C=80=EC=83=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - detect_gear_groups: vessel_dfs(분류 대상만) → vessel_store.get_all_latest_positions()(전체 14K선박) - build_all_group_snapshots: 동일하게 all_positions 기반으로 전환 - vessel_store: get_all_latest_positions() 메서드 추가 - 결과: 0 gear groups → 210 gear groups (GEAR_IN_ZONE 57, GEAR_OUT_ZONE 45) --- prediction/algorithms/polygon_builder.py | 152 ++++++++++------------- prediction/cache/vessel_store.py | 18 +++ prediction/scheduler.py | 4 +- 3 files changed, 85 insertions(+), 89 deletions(-) diff --git a/prediction/algorithms/polygon_builder.py b/prediction/algorithms/polygon_builder.py index 5f014ac..4fe7036 100644 --- a/prediction/algorithms/polygon_builder.py +++ b/prediction/algorithms/polygon_builder.py @@ -108,17 +108,16 @@ def build_group_polygon( def detect_gear_groups( - vessel_dfs: dict, vessel_store, now: Optional[datetime] = None, ) -> list[dict]: """어구 이름 패턴으로 어구그룹을 탐지한다. 프론트엔드 FleetClusterLayer.tsx gearGroupMap useMemo 로직 이관. + 전체 AIS 선박(vessel_store._tracks)에서 어구 패턴을 탐지한다. Args: - vessel_dfs: {mmsi: DataFrame} — 각 DataFrame은 lat, lon, sog, cog, timestamp 칼럼. - vessel_store: VesselStore — get_vessel_info(mmsi) → {name, ...}. + vessel_store: VesselStore — get_all_latest_positions() + get_vessel_info(). now: 기준 시각 (None이면 UTC now). Returns: @@ -127,46 +126,40 @@ def detect_gear_groups( if now is None: now = datetime.now(timezone.utc) - # 선박명 → mmsi 맵 (모선 탐색용) + # 전체 선박의 최신 위치 가져오기 + all_positions = vessel_store.get_all_latest_positions() + + # 선박명 → mmsi 맵 (모선 탐색용, 어구 패턴이 아닌 선박만) name_to_mmsi: dict[str, str] = {} - for mmsi, df in vessel_dfs.items(): - if df is None or len(df) == 0: - continue - info = vessel_store.get_vessel_info(mmsi) - name: str = (info or {}).get('name', '') or '' - name = name.strip() + for mmsi, pos in all_positions.items(): + name = (pos.get('name') or '').strip() if name and not GEAR_PATTERN.match(name): name_to_mmsi[name] = mmsi # 1단계: 같은 모선명 어구 수집 (60분 이내만) raw_groups: dict[str, list[dict]] = {} - for mmsi, df in vessel_dfs.items(): - if df is None or len(df) == 0: + for mmsi, pos in all_positions.items(): + name = (pos.get('name') or '').strip() + if not name: continue - last = df.iloc[-1] - ts = last.get('timestamp') if hasattr(last, 'get') else last['timestamp'] - # timestamp → datetime 변환 - if isinstance(ts, datetime): - last_dt = ts if ts.tzinfo is not None else ts.replace(tzinfo=timezone.utc) - else: - # pandas Timestamp 또는 숫자(unix seconds) - try: - import pandas as pd - last_dt = pd.Timestamp(ts).to_pydatetime() - if last_dt.tzinfo is None: - last_dt = last_dt.replace(tzinfo=timezone.utc) - except Exception: + # staleness 체크 + ts = pos.get('timestamp') + if ts is not None: + if isinstance(ts, datetime): + last_dt = ts if ts.tzinfo is not None else ts.replace(tzinfo=timezone.utc) + else: + try: + import pandas as pd + last_dt = pd.Timestamp(ts).to_pydatetime() + if last_dt.tzinfo is None: + last_dt = last_dt.replace(tzinfo=timezone.utc) + except Exception: + continue + age_sec = (now - last_dt).total_seconds() + if age_sec > STALE_SEC: continue - age_sec = (now - last_dt).total_seconds() - if age_sec > STALE_SEC: - continue - - info = vessel_store.get_vessel_info(mmsi) - name = (info or {}).get('name', '') or '' - name = name.strip() - m = GEAR_PATTERN.match(name) if not m: continue @@ -175,10 +168,10 @@ def detect_gear_groups( entry = { 'mmsi': mmsi, 'name': name, - 'lat': float(last['lat']), - 'lon': float(last['lon']), - 'sog': float(last.get('sog', 0) if hasattr(last, 'get') else last['sog']), - 'cog': float(last.get('cog', 0) if hasattr(last, 'get') else last['cog']), + 'lat': pos['lat'], + 'lon': pos['lon'], + 'sog': pos.get('sog', 0), + 'cog': pos.get('cog', 0), } raw_groups.setdefault(parent_name, []).append(entry) @@ -191,12 +184,10 @@ def detect_gear_groups( anchor_lat: Optional[float] = None anchor_lon: Optional[float] = None - if parent_mmsi and parent_mmsi in vessel_dfs: - parent_df = vessel_dfs[parent_mmsi] - if parent_df is not None and len(parent_df) > 0: - parent_last = parent_df.iloc[-1] - anchor_lat = float(parent_last['lat']) - anchor_lon = float(parent_last['lon']) + if parent_mmsi and parent_mmsi in all_positions: + parent_pos = all_positions[parent_mmsi] + anchor_lat = parent_pos['lat'] + anchor_lon = parent_pos['lon'] if anchor_lat is None and gears: anchor_lat = gears[0]['lat'] @@ -240,7 +231,6 @@ def detect_gear_groups( def build_all_group_snapshots( - vessel_dfs: dict, vessel_store, company_vessels: dict[int, list[str]], companies: dict[int, dict], @@ -250,8 +240,7 @@ def build_all_group_snapshots( Shapely 미설치 시 빈 리스트를 반환한다. Args: - vessel_dfs: {mmsi: DataFrame}. - vessel_store: VesselStore — get_vessel_info(mmsi). + vessel_store: VesselStore — get_all_latest_positions() + get_vessel_info(). company_vessels: {company_id: [mmsi_list]}. companies: {id: {name_cn, name_en}}. @@ -264,6 +253,7 @@ def build_all_group_snapshots( now = datetime.now(timezone.utc) snapshots: list[dict] = [] + all_positions = vessel_store.get_all_latest_positions() # ── FLEET 타입: company_vessels 순회 ────────────────────────── for company_id, mmsi_list in company_vessels.items(): @@ -275,20 +265,17 @@ def build_all_group_snapshots( members: list[dict] = [] for mmsi in mmsi_list: - df = vessel_dfs.get(mmsi) - if df is None or len(df) == 0: + pos = all_positions.get(mmsi) + if not pos: continue - last = df.iloc[-1] - lat = float(last['lat']) - lon = float(last['lon']) - sog_val = last.get('sog', 0) if hasattr(last, 'get') else last['sog'] - cog_val = last.get('cog', 0) if hasattr(last, 'get') else last['cog'] - sog = float(sog_val) - cog = float(cog_val) + lat = pos['lat'] + lon = pos['lon'] + sog = pos.get('sog', 0) + cog = pos.get('cog', 0) points.append((lon, lat)) members.append({ 'mmsi': mmsi, - 'name': (vessel_store.get_vessel_info(mmsi) or {}).get('name', ''), + 'name': pos.get('name', ''), 'lat': lat, 'lon': lon, 'sog': sog, @@ -321,7 +308,7 @@ def build_all_group_snapshots( }) # ── GEAR 타입: detect_gear_groups 결과 순회 ─────────────────── - gear_groups = detect_gear_groups(vessel_dfs, vessel_store, now=now) + gear_groups = detect_gear_groups(vessel_store, now=now) for group in gear_groups: parent_name: str = group['parent_name'] @@ -332,12 +319,10 @@ def build_all_group_snapshots( anchor_lat: Optional[float] = None anchor_lon: Optional[float] = None - if parent_mmsi and parent_mmsi in vessel_dfs: - parent_df = vessel_dfs.get(parent_mmsi) - if parent_df is not None and len(parent_df) > 0: - p_last = parent_df.iloc[-1] - anchor_lat = float(p_last['lat']) - anchor_lon = float(p_last['lon']) + if parent_mmsi and parent_mmsi in all_positions: + parent_pos = all_positions[parent_mmsi] + anchor_lat = parent_pos['lat'] + anchor_lon = parent_pos['lon'] if anchor_lat is None and gear_members: anchor_lat = gear_members[0]['lat'] @@ -357,14 +342,11 @@ def build_all_group_snapshots( # 폴리곤 points: 어구 좌표 + 모선 좌표 points = [(g['lon'], g['lat']) for g in gear_members] - if parent_mmsi and parent_mmsi in vessel_dfs: - parent_df = vessel_dfs.get(parent_mmsi) - if parent_df is not None and len(parent_df) > 0: - p_last = parent_df.iloc[-1] - p_lat = float(p_last['lat']) - p_lon = float(p_last['lon']) - if (p_lon, p_lat) not in points: - points.append((p_lon, p_lat)) + if parent_mmsi and parent_mmsi in all_positions: + parent_pos = all_positions[parent_mmsi] + p_lon, p_lat = parent_pos['lon'], parent_pos['lat'] + if (p_lon, p_lat) not in points: + points.append((p_lon, p_lat)) polygon_wkt, center_wkt, area_sq_nm, _clat, _clon = build_group_polygon( points, GEAR_BUFFER_DEG @@ -373,22 +355,18 @@ def build_all_group_snapshots( # members JSONB 구성 members_out: list[dict] = [] # 모선 먼저 - if parent_mmsi and parent_mmsi in vessel_dfs: - parent_df = vessel_dfs.get(parent_mmsi) - if parent_df is not None and len(parent_df) > 0: - p_last = parent_df.iloc[-1] - p_sog = float(p_last.get('sog', 0) if hasattr(p_last, 'get') else p_last['sog']) - p_cog = float(p_last.get('cog', 0) if hasattr(p_last, 'get') else p_last['cog']) - members_out.append({ - 'mmsi': parent_mmsi, - 'name': parent_name, - 'lat': float(p_last['lat']), - 'lon': float(p_last['lon']), - 'sog': p_sog, - 'cog': p_cog, - 'role': 'PARENT', - 'isParent': True, - }) + if parent_mmsi and parent_mmsi in all_positions: + parent_pos = all_positions[parent_mmsi] + members_out.append({ + 'mmsi': parent_mmsi, + 'name': parent_name, + 'lat': parent_pos['lat'], + 'lon': parent_pos['lon'], + 'sog': parent_pos.get('sog', 0), + 'cog': parent_pos.get('cog', 0), + 'role': 'PARENT', + 'isParent': True, + }) # 어구 목록 for g in gear_members: members_out.append({ diff --git a/prediction/cache/vessel_store.py b/prediction/cache/vessel_store.py index 550ba89..f16b711 100644 --- a/prediction/cache/vessel_store.py +++ b/prediction/cache/vessel_store.py @@ -317,6 +317,24 @@ class VesselStore: """Return static vessel info dict for the given MMSI, or empty dict if not found.""" return self._static_info.get(mmsi, {}) + def get_all_latest_positions(self) -> dict[str, dict]: + """모든 선박의 최신 위치 반환. {mmsi: {lat, lon, sog, cog, timestamp, name}}""" + result: dict[str, dict] = {} + for mmsi, df in self._tracks.items(): + if df is None or len(df) == 0: + continue + last = df.iloc[-1] + info = self._static_info.get(mmsi, {}) + result[mmsi] = { + 'lat': float(last['lat']), + 'lon': float(last['lon']), + 'sog': float(last.get('sog', 0) or 0), + 'cog': float(last.get('cog', 0) or 0), + 'timestamp': last.get('timestamp'), + 'name': info.get('name', ''), + } + return result + # ------------------------------------------------------------------ # Properties # ------------------------------------------------------------------ diff --git a/prediction/scheduler.py b/prediction/scheduler.py index 1cbd9a7..5360a3b 100644 --- a/prediction/scheduler.py +++ b/prediction/scheduler.py @@ -104,9 +104,9 @@ def run_analysis_cycle(): from algorithms.polygon_builder import detect_gear_groups, build_all_group_snapshots company_vessels = fleet_tracker.get_company_vessels(vessel_dfs) - gear_groups = detect_gear_groups(vessel_dfs, vessel_store) + gear_groups = detect_gear_groups(vessel_store) group_snapshots = build_all_group_snapshots( - vessel_dfs, vessel_store, company_vessels, + vessel_store, company_vessels, fleet_tracker._companies, ) saved = kcgdb.save_group_snapshots(group_snapshots) -- 2.45.2