Files
static/lib/weekly_visitor_forecast.py

270 lines
9.2 KiB
Python

# 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()