# weekly_visitor_forecast.py import os, sys sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from datetime import date, timedelta, datetime from collections import defaultdict import pandas as pd from sqlalchemy import select, func from weather_forecast import get_weekly_precip from conf import db, db_schema from lib.holiday import is_korean_holiday from lib.common import load_config config = load_config() visitor_ca_filter = config.get('POS', {}).get('VISITOR_CA', []) ga4_by_date = db_schema.ga4_by_date weather = db_schema.weather air = db_schema.air pos = db_schema.pos engine = db.engine def get_recent_dates(today=None, days=14): today = today or date.today() return [today - timedelta(days=i) for i in reversed(range(days))] def get_this_week_dates(today=None): today = today or date.today() weekday = today.weekday() return [today + timedelta(days=i) for i in range(7 - weekday)] def get_last_year_same_weekdays(dates): """ 작년의 동일한 요일을 가진 날짜를 반환 (예: 2025-07-08(화) → 2024년 중 가장 가까운 화요일) Args: dates (list of datetime.date): 기준 날짜 리스트 Returns: list of datetime.date: 작년의 동일 요일 날짜 리스트 """ result = [] for d in dates: target_weekday = d.weekday() last_year_date = d - timedelta(days=365) # 동일 요일 찾기: 주중 1주 범위 내에서 동일 요일 탐색 delta = 0 found = None for offset in range(-3, 4): candidate = last_year_date + timedelta(days=offset) if candidate.weekday() == target_weekday: found = candidate break if found: result.append(found) else: result.append(last_year_date) # fallback return result def pm25_grade(value): if value is None: return '' if value <= 15: return '좋음' elif value <= 35: return '보통' elif value <= 75: return '나쁨' else: return '매우나쁨' def fetch_data_for_dates(date_list): session = db.get_session() data = defaultdict(dict) try: # GA4 activeUsers stmt = ( select(ga4_by_date.c.date, func.sum(ga4_by_date.c.activeUsers)) .where(ga4_by_date.c.date.in_(date_list)) .group_by(ga4_by_date.c.date) ) for d, val in session.execute(stmt): data[d]['웹 방문자 수'] = val # POS 입장객 수 stmt = ( select(pos.c.date, func.sum(pos.c.qty)) .where( (pos.c.date.in_(date_list)) & (pos.c.ca01 == '매표소') & (pos.c.ca03.in_(visitor_ca_filter)) ) .group_by(pos.c.date) ) for d, val in session.execute(stmt): data[d]['입장객 수'] = val # 날씨 정보 stmt = ( select( weather.c.date, func.min(weather.c.minTa), func.max(weather.c.maxTa), func.avg(weather.c.avgRhm), func.sum(weather.c.sumRn) ) .where(weather.c.date.in_(date_list)) .group_by(weather.c.date) ) for row in session.execute(stmt): d, minTa, maxTa, rhm, rn = row data[d]['최저기온'] = round(minTa or 0, 1) data[d]['최고기온'] = round(maxTa or 0, 1) data[d]['습도'] = round(rhm or 0, 1) data[d]['강수량'] = round(rn or 0, 1) # 미세먼지 (pm25) stmt = ( select(air.c.date, func.avg(air.c.pm25)) .where(air.c.date.in_(date_list)) .group_by(air.c.date) ) for d, pm25 in session.execute(stmt): data[d]['미세먼지'] = pm25_grade(pm25) finally: session.close() return data def load_prophet_forecast(file_path=None): if file_path is None: file_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'data', 'prophet_result.csv')) print(f"[DEBUG] Load prophet forecast from: {file_path}") if not os.path.exists(file_path): print(f"[ERROR] 파일이 존재하지 않습니다: {file_path}") return pd.Series(dtype=float) try: df = pd.read_csv(file_path) # 컬럼명 출력 확인 print(f"[DEBUG] CSV columns: {df.columns.tolist()}") if 'date' not in df.columns or 'visitor_forecast' not in df.columns: print("[ERROR] 필요한 컬럼이 CSV에 없습니다.") return pd.Series(dtype=float) df['date'] = pd.to_datetime(df['date']) df.set_index('date', inplace=True) return df['visitor_forecast'] except Exception as e: print(f"[ERROR] Prophet 예측 결과 불러오기 실패: {e}") return pd.Series(dtype=float) def build_dataframe(dates, data, use_forecast_after=None): """ use_forecast_after: datetime.date or None 지정한 날짜 이후부터는 '예상 방문자'로 '입장객 수' 대체 """ records = [] for d in dates: predicted = data.get(d, {}).get('예상 방문자') if use_forecast_after is not None and d >= use_forecast_after and predicted is not None: 입장객수 = int(predicted) else: 입장객수 = data.get(d, {}).get('입장객 수', 0) r = { '날짜': d.strftime('%Y-%m-%d'), '요일': ['월', '화', '수', '목', '금', '토', '일'][d.weekday()], '공휴일': '✅' if is_korean_holiday(d) else '', '웹 방문자 수': data.get(d, {}).get('웹 방문자 수', 0), '입장객 수': 입장객수, '최저기온': data.get(d, {}).get('최저기온', ''), '최고기온': data.get(d, {}).get('최고기온', ''), '습도': data.get(d, {}).get('습도', ''), '강수량': data.get(d, {}).get('강수량', ''), '미세먼지': data.get(d, {}).get('미세먼지', ''), } records.append(r) return pd.DataFrame(records) def main(): today = date.today() # 이번 주 일요일 (주말) weekday = today.weekday() sunday = today + timedelta(days=(6 - weekday)) # 최근 2주 및 작년 동일 요일 (최근 2주는 sunday까지 포함) recent_dates = [sunday - timedelta(days=i) for i in reversed(range(14))] prev_year_dates = get_last_year_same_weekdays(recent_dates) # 이번 주 예상 대상 (오늘부터 일요일까지) this_week_dates = [today + timedelta(days=i) for i in range(7 - weekday)] # 데이터 조회 recent_data = fetch_data_for_dates(recent_dates) prev_year_data = fetch_data_for_dates(prev_year_dates) forecast_data = fetch_data_for_dates(this_week_dates) # 결측 강수량 보정 - 오늘 이후 날짜가 비어있거나 강수량 없으면 날씨예보로 채움 weekly_precip = get_weekly_precip(load_config()['DATA_API']['serviceKey']) for d in recent_dates: if d >= today and (d not in recent_data or '강수량' not in recent_data[d]): dt_str = d.strftime('%Y%m%d') if dt_str in weekly_precip: if d not in recent_data: recent_data[d] = {} recent_data[d]['강수량'] = round(float(weekly_precip[dt_str]['sumRn']), 1) recent_data[d]['최저기온'] = round(float(weekly_precip[dt_str]['minTa']), 1) recent_data[d]['최고기온'] = round(float(weekly_precip[dt_str]['maxTa']), 1) recent_data[d]['습도'] = round(float(weekly_precip[dt_str]['avgRhm']), 1) # prophet 예측 결과 불러오기 및 이번 주 예상 데이터에 병합 prophet_forecast = load_prophet_forecast() for d in this_week_dates: d_ts = pd.Timestamp(d) has_forecast = d_ts in prophet_forecast.index print(f"[DEBUG] 날짜 {d} (Timestamp {d_ts}) 예측 데이터 존재 여부: {has_forecast}") if has_forecast: if d not in forecast_data: forecast_data[d] = {} forecast_data[d]['예상 방문자'] = round(float(prophet_forecast.loc[d_ts]), 0) else: if d not in forecast_data: forecast_data[d] = {} forecast_data[d]['예상 방문자'] = None # 최근 2주 데이터에도 오늘 이후 날짜에 대해 예상 방문자 병합 for d in recent_dates: d_ts = pd.Timestamp(d) if d >= today and d_ts in prophet_forecast.index: if d not in recent_data: recent_data[d] = {} recent_data[d]['예상 방문자'] = round(float(prophet_forecast.loc[d_ts]), 0) # 데이터프레임 생성 df_recent = build_dataframe(recent_dates, recent_data, use_forecast_after=today) df_prev = build_dataframe(prev_year_dates, prev_year_data) # 출력 설정 pd.set_option('display.unicode.east_asian_width', True) pd.set_option('display.max_columns', None) pd.set_option('display.width', 200) print("📊 최근 2주간 방문자 현황:") print(df_recent.to_string(index=False)) print("\n📈 작년 동일 요일 데이터:") print(df_prev.to_string(index=False)) if __name__ == "__main__": main()