# =================================================================== # services/analytics/visitor_forecast.py # 방문객 예측 서비스 모듈 # =================================================================== # 날씨, 휴일, 과거 데이터를 기반으로 방문객 수를 예측합니다. # 간단한 가중치 기반 모델과 Prophet 시계열 모델을 지원합니다. # =================================================================== """ 방문객 예측 서비스 모듈 날씨 조건, 휴일 여부, 과거 방문 패턴을 분석하여 미래 방문객 수를 예측합니다. 사용 예시: from services.analytics.visitor_forecast import VisitorForecaster forecaster = VisitorForecaster(config) predictions = forecaster.predict_weekly() """ from datetime import datetime, timedelta, date from typing import Dict, List, Optional, Tuple, Any from core.logging_utils import get_logger from core.config import get_config logger = get_logger(__name__) class VisitorForecaster: """ 방문객 예측 클래스 다양한 요소를 고려하여 방문객 수를 예측합니다. Attributes: weights: 예측 가중치 설정 visitor_multiplier: 최종 예측값 조정 계수 """ def __init__( self, weights: Optional[Dict] = None, visitor_multiplier: float = 0.5 ): """ Args: weights: 예측 가중치 (None이면 설정에서 로드) visitor_multiplier: 예측값 조정 계수 """ if weights is None: config = get_config() forecast_config = config.forecast_weight self.weights = { 'min_temp': forecast_config.get('min_temp', 1.0), 'max_temp': forecast_config.get('max_temp', 1.0), 'precipitation': forecast_config.get('precipitation', 10.0), 'humidity': forecast_config.get('humidity', 1.0), 'pm25': forecast_config.get('pm25', 1.0), 'holiday': forecast_config.get('holiday', 20), } self.visitor_multiplier = forecast_config.get('visitor_multiplier', 0.5) else: self.weights = weights self.visitor_multiplier = visitor_multiplier def calculate_weather_impact( self, min_temp: float, max_temp: float, precipitation: float, humidity: float, pm25: Optional[float] = None ) -> float: """ 날씨 조건에 따른 방문객 영향도 계산 각 날씨 요소가 방문객 수에 미치는 영향을 계산합니다. 높은 값일수록 방문객 수 감소를 의미합니다. Args: min_temp: 최저 기온 (℃) max_temp: 최고 기온 (℃) precipitation: 강수량 (mm) humidity: 습도 (%) pm25: 초미세먼지 농도 (㎍/㎥) Returns: 날씨 영향도 점수 (높을수록 부정적) """ impact = 0.0 # 기온 영향 (너무 낮거나 높으면 부정적) # 최적 온도: 15~25℃ if min_temp < 0: impact += abs(min_temp) * self.weights['min_temp'] elif min_temp < 10: impact += (10 - min_temp) * self.weights['min_temp'] * 0.3 if max_temp > 35: impact += (max_temp - 35) * self.weights['max_temp'] elif max_temp > 30: impact += (max_temp - 30) * self.weights['max_temp'] * 0.5 # 강수량 영향 (비가 오면 크게 부정적) if precipitation > 0: impact += precipitation * self.weights['precipitation'] # 습도 영향 if humidity > 80: impact += (humidity - 80) * self.weights['humidity'] * 0.1 # 미세먼지 영향 if pm25 is not None: if pm25 > 75: # 나쁨 기준 impact += (pm25 - 75) * self.weights['pm25'] * 0.5 elif pm25 > 35: # 보통 기준 impact += (pm25 - 35) * self.weights['pm25'] * 0.2 return impact def calculate_holiday_impact(self, is_holiday: bool, is_weekend: bool) -> float: """ 휴일/주말에 따른 방문객 영향도 계산 Args: is_holiday: 공휴일 여부 is_weekend: 주말 여부 Returns: 휴일 영향도 (양수: 방문객 증가, 음수: 감소) """ if is_holiday: return self.weights['holiday'] elif is_weekend: return self.weights['holiday'] * 0.7 else: return 0.0 def predict_visitors( self, base_visitors: float, weather_data: Dict, is_holiday: bool = False, is_weekend: bool = False ) -> float: """ 방문객 수 예측 기준 방문객 수에 날씨와 휴일 영향을 적용하여 예측합니다. Args: base_visitors: 기준 방문객 수 (과거 평균) weather_data: 날씨 데이터 딕셔너리 - min_temp: 최저 기온 - max_temp: 최고 기온 - precipitation: 강수량 (또는 sumRn) - humidity: 습도 (또는 avgRhm) - pm25: 미세먼지 (선택) is_holiday: 공휴일 여부 is_weekend: 주말 여부 Returns: 예측 방문객 수 """ # 날씨 데이터 추출 min_temp = weather_data.get('min_temp', weather_data.get('minTa', 15)) max_temp = weather_data.get('max_temp', weather_data.get('maxTa', 25)) precipitation = weather_data.get('precipitation', weather_data.get('sumRn', 0)) humidity = weather_data.get('humidity', weather_data.get('avgRhm', 50)) pm25 = weather_data.get('pm25') # 영향도 계산 weather_impact = self.calculate_weather_impact( min_temp, max_temp, precipitation, humidity, pm25 ) holiday_impact = self.calculate_holiday_impact(is_holiday, is_weekend) # 예측값 계산 # 날씨 영향은 감소 효과, 휴일 영향은 증가 효과 adjustment = holiday_impact - weather_impact # 조정 계수 적용 predicted = base_visitors * (1 + adjustment / 100 * self.visitor_multiplier) # 최소값 보장 return max(0, predicted) def predict_weekly( self, base_visitors: float, weekly_weather: Dict[str, Dict], holidays: Optional[List[date]] = None ) -> Dict[str, float]: """ 주간 방문객 예측 Args: base_visitors: 기준 방문객 수 weekly_weather: 일별 날씨 데이터 {YYYYMMDD: weather_data} holidays: 휴일 목록 Returns: 일별 예측 방문객 {YYYYMMDD: visitors} """ if holidays is None: holidays = [] predictions = {} for date_str, weather in weekly_weather.items(): try: dt = datetime.strptime(date_str, '%Y%m%d').date() is_holiday = dt in holidays is_weekend = dt.weekday() >= 5 predicted = self.predict_visitors( base_visitors, weather, is_holiday, is_weekend ) predictions[date_str] = round(predicted) logger.debug( f"{date_str}: 기준={base_visitors}, " f"휴일={is_holiday}, 주말={is_weekend}, " f"예측={predicted:.0f}" ) except Exception as e: logger.warning(f"예측 실패 ({date_str}): {e}") predictions[date_str] = base_visitors return predictions def analyze_prediction_factors( self, weather_data: Dict, is_holiday: bool = False, is_weekend: bool = False ) -> Dict[str, Any]: """ 예측 요인 분석 각 요인이 예측에 미치는 영향을 분석합니다. Args: weather_data: 날씨 데이터 is_holiday: 공휴일 여부 is_weekend: 주말 여부 Returns: 요인별 영향 분석 결과 """ min_temp = weather_data.get('min_temp', weather_data.get('minTa', 15)) max_temp = weather_data.get('max_temp', weather_data.get('maxTa', 25)) precipitation = weather_data.get('precipitation', weather_data.get('sumRn', 0)) humidity = weather_data.get('humidity', weather_data.get('avgRhm', 50)) pm25 = weather_data.get('pm25') analysis = { 'weather': { 'min_temp': { 'value': min_temp, 'impact': abs(min_temp) * self.weights['min_temp'] if min_temp < 0 else 0 }, 'max_temp': { 'value': max_temp, 'impact': (max_temp - 35) * self.weights['max_temp'] if max_temp > 35 else 0 }, 'precipitation': { 'value': precipitation, 'impact': precipitation * self.weights['precipitation'] if precipitation > 0 else 0 }, 'humidity': { 'value': humidity, 'impact': (humidity - 80) * self.weights['humidity'] * 0.1 if humidity > 80 else 0 } }, 'holiday': { 'is_holiday': is_holiday, 'is_weekend': is_weekend, 'impact': self.calculate_holiday_impact(is_holiday, is_weekend) }, 'total_weather_impact': self.calculate_weather_impact( min_temp, max_temp, precipitation, humidity, pm25 ), 'total_holiday_impact': self.calculate_holiday_impact(is_holiday, is_weekend) } if pm25 is not None: analysis['weather']['pm25'] = { 'value': pm25, 'impact': (pm25 - 75) * self.weights['pm25'] * 0.5 if pm25 > 75 else 0 } return analysis