339 lines
12 KiB
Python
339 lines
12 KiB
Python
# ===================================================================
|
|
# 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
|
|
|
|
|
|
if __name__ == '__main__':
|
|
"""
|
|
방문객 예측 서비스 모듈 테스트
|
|
|
|
사용법:
|
|
python services/analytics/visitor_forecast.py
|
|
"""
|
|
logger.info("=== 방문객 예측 서비스 모듈 테스트 ===")
|
|
|
|
try:
|
|
config = get_config()
|
|
|
|
logger.info(f"설정 로드 완료")
|
|
|
|
# 예측기 초기화
|
|
forecaster = VisitorForecaster(config)
|
|
logger.info("\nVisitorForecaster 초기화 완료")
|
|
|
|
logger.info("\n제공 기능:")
|
|
logger.info("- predict_daily: 일별 방문객 수 예측")
|
|
logger.info("- predict_weekly: 주별 방문객 수 예측")
|
|
logger.info("- analyze_weather_impact: 날씨 영향도 분석")
|
|
logger.info("- calculate_holiday_impact: 휴일 영향도 계산")
|
|
|
|
logger.info("\n예측 요인:")
|
|
logger.info("- 날씨 (기온, 강수량, 습도)")
|
|
logger.info("- 휴일 여부")
|
|
logger.info("- 주말 여부")
|
|
logger.info("- 과거 방문 패턴")
|
|
|
|
logger.info("\n✓ 방문객 예측 서비스 모듈 테스트 완료")
|
|
|
|
except Exception as e:
|
|
logger.error(f"방문객 예측 모듈 테스트 실패: {e}")
|
|
import traceback
|
|
logger.error(traceback.format_exc())
|