modu-lightway / src /data_schema.py
Munsusu's picture
Commit message: Initial deploy — 모두의 빛길 v1.0
131589b verified
"""모두의 빛길 — 노드/엣지 데이터 스키마 정의.
이 모듈은 추천 그래프를 구성하는 모든 노드 타입을 dataclass로 정의한다.
PyG HeteroData에 그대로 매핑할 수 있도록 단순한 평면 구조로 유지한다.
노드 타입
---------
- VenueNode : 문화시설 (박물관/미술관/공연장/도서관 등)
- EventNode : 공연·전시 이벤트
- TransitNode : 지하철역·버스정류장
- AmenityNode : 화장실/쉼터/벤치/CCTV
- HazardNode : 사고다발지역/공사구간/저조도
엣지 타입
---------
- (VENUE)-hosts->(EVENT)
- (VENUE)<->(TRANSIT) near
- (VENUE)<->(AMENITY) has_amenity
- (TRANSIT)<->(TRANSIT) walkable / connects
- (TRANSIT)->(HAZARD) passes_hazard
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Optional
# ============================================================
# 노드 dataclass
# ============================================================
@dataclass
class VenueNode:
"""문화시설 노드."""
id: int
name: str
lat: float
lng: float
venue_type: str # 박물관/미술관/공연장/도서관/카페/문화센터
accessibility_score: float = 0.0 # 0~1, 무장애 종합점수
has_elevator: bool = False
has_disabled_toilet: bool = False
has_ramp: bool = False
free: bool = False
indoor: bool = True
age_friendly: bool = False # 어르신 무료/할인 등
booking_url: str = ""
phone: str = ""
def feature_vec(self) -> List[float]:
"""GNN 입력 feature 벡터."""
return [
self.accessibility_score,
float(self.has_elevator),
float(self.has_disabled_toilet),
float(self.has_ramp),
float(self.free),
float(self.indoor),
float(self.age_friendly),
# venue_type one-hot (8종)
*self._type_onehot(),
]
def _type_onehot(self) -> List[float]:
types = ["박물관", "미술관", "공연장", "도서관", "카페", "문화센터", "체험관", "기타"]
v = [0.0] * len(types)
if self.venue_type in types:
v[types.index(self.venue_type)] = 1.0
else:
v[-1] = 1.0
return v
@dataclass
class EventNode:
"""공연·전시 이벤트 노드."""
id: int
venue_id: int
title: str
start_time: str # "HH:MM"
end_time: str
age_limit: Optional[int] = None
price: int = 0
booking_url: str = ""
event_type: str = "공연" # 공연/전시/체험/강좌
has_subtitle: bool = False # 자막
has_sign_lang: bool = False # 수어 통역
has_audio_guide: bool = False # 음성해설/점자
def feature_vec(self) -> List[float]:
sh, sm = map(int, self.start_time.split(":"))
eh, em = map(int, self.end_time.split(":"))
return [
sh + sm / 60.0,
eh + em / 60.0,
float(self.age_limit or 0),
float(self.price),
float(self.has_subtitle),
float(self.has_sign_lang),
float(self.has_audio_guide),
float(self.event_type == "공연"),
float(self.event_type == "전시"),
float(self.event_type == "체험"),
float(self.event_type == "강좌"),
]
@dataclass
class TransitNode:
"""지하철역·버스정류장 노드."""
id: int
name: str
lat: float
lng: float
transit_type: str = "지하철" # 지하철/버스/저상버스
has_elevator: bool = False
has_toilet: bool = False
line: str = ""
def feature_vec(self) -> List[float]:
return [
float(self.has_elevator),
float(self.has_toilet),
float(self.transit_type == "지하철"),
float(self.transit_type == "저상버스"),
float(self.transit_type == "버스"),
]
@dataclass
class AmenityNode:
"""편의시설 노드 — 화장실/쉼터/벤치/CCTV."""
id: int
name: str
lat: float
lng: float
amenity_type: str # 공공화장실/장애인화장실/무더위쉼터/벤치/CCTV/가로등
accessible: bool = False
open_24h: bool = False
def feature_vec(self) -> List[float]:
return [
float(self.accessible),
float(self.open_24h),
float(self.amenity_type == "공공화장실"),
float(self.amenity_type == "장애인화장실"),
float(self.amenity_type == "무더위쉼터"),
float(self.amenity_type == "벤치"),
float(self.amenity_type == "CCTV"),
float(self.amenity_type == "가로등"),
]
@dataclass
class HazardNode:
"""위험구간 노드 — 사고다발지역/공사구간/저조도."""
id: int
lat: float
lng: float
hazard_type: str # 보행자사고다발/노인사고다발/공사/저조도/계단/턱
severity: float = 0.5 # 0~1
night_only: bool = False # 야간에만 위험
def feature_vec(self) -> List[float]:
return [
self.severity,
float(self.night_only),
float(self.hazard_type == "보행자사고다발"),
float(self.hazard_type == "노인사고다발"),
float(self.hazard_type == "공사"),
float(self.hazard_type == "저조도"),
float(self.hazard_type == "계단"),
float(self.hazard_type == "턱"),
]
# ============================================================
# 컬렉션 컨테이너
# ============================================================
@dataclass
class GraphData:
"""GraphBuilder에 입력될 컬렉션."""
venues: List[VenueNode] = field(default_factory=list)
events: List[EventNode] = field(default_factory=list)
transits: List[TransitNode] = field(default_factory=list)
amenities: List[AmenityNode] = field(default_factory=list)
hazards: List[HazardNode] = field(default_factory=list)
def stats(self) -> dict:
return {
"VENUE": len(self.venues),
"EVENT": len(self.events),
"TRANSIT": len(self.transits),
"AMENITY": len(self.amenities),
"HAZARD": len(self.hazards),
}
# ============================================================
# 슬롯 정의 (slot_extractor와 공유)
# ============================================================
SLOT_NAMES = [
"USER_TYPE", # 고령자/휠체어사용자/임산부/시각장애/청각장애/어린이동반/보행보조기
"COMPANION", # 가족/보호자/활동지원사/단체
"ORIGIN", # 병원/복지관/지하철역/자택
"WALK_LIMIT", # 짧음/거리지정/환승최소
"AVOID", # 위험회피/계단회피/경사회피/혼잡회피/사고다발회피/야외회피
"CULTURE_PREF", # 전시/공연/박물관/미술관/도서관/카페/체험/문화일반/미디어아트/전통문화
"WEATHER", # 폭염/한파/강우/미세먼지
"TIME_WINDOW", # 오전/낮/오후/저녁(야간)/시간한정
"BUDGET", # 무료/할인/저예산
"SPECIAL_NEEDS", # 화장실/수유실/안내견/점자/수어자막/엘리베이터/장애인화장실
]
__all__ = [
"VenueNode", "EventNode", "TransitNode", "AmenityNode", "HazardNode",
"GraphData", "SLOT_NAMES",
]