-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.py
More file actions
220 lines (178 loc) · 7.13 KB
/
Copy pathmain.py
File metadata and controls
220 lines (178 loc) · 7.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from datetime import datetime, timedelta
import asyncio
import logging
import os
import pickle
import requests
import holidays
import numpy as np
# (로컬 개발 편의를 위해) .env 지원 — 프로덕션(App Service)은 앱 설정으로 주입됨
try:
from dotenv import load_dotenv # optional
load_dotenv()
except Exception:
pass
# ========= FastAPI 기본 설정 =========
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
app = FastAPI(title="predictainer")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 필요시 제한
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ========= 전역 상태 =========
API_KEY = os.getenv("API_KEY")
if not API_KEY:
logging.warning("API_KEY env not set. Set it in App Service > Configuration > Application settings.")
models_ready: bool = False
load_error: str | None = None
# 모델/스케일러 전역 핸들 (로드 후 할당)
lgb_model = None
cat_model = None
mlp_model = None
scaler = None
# ========= 모델 로더 =========
def _blocking_load_models():
"""블로킹 모델 로드: 별도 스레드에서 실행"""
import lightgbm as lgb
from catboost import CatBoostRegressor
import tensorflow as tf
global lgb_model, cat_model, mlp_model, scaler
# 경로는 컨테이너 /app 기준
lgb_model = lgb.Booster(model_file="models/lgb_model.txt")
cat = CatBoostRegressor()
cat.load_model("models/cat_model.cbm")
cat_model = cat
mlp_model = tf.keras.models.load_model("models/mlp_model.h5", compile=False)
with open("models/scaler.pkl", "rb") as f:
_scaler = pickle.load(f)
globals()["scaler"] = _scaler
async def load_models_async():
"""이벤트 루프를 막지 않도록, 블로킹 로드를 워커 스레드로 넘김"""
start = datetime.utcnow()
logging.info("🔄 Loading models in background...")
await asyncio.to_thread(_blocking_load_models)
took = (datetime.utcnow() - start).total_seconds()
logging.info(f"✅ Models loaded (took {took:.1f}s)")
# ========= 앱 라이프사이클: 서버가 포트를 연 뒤 백그라운드로 로드 =========
@asynccontextmanager
async def lifespan(app: FastAPI):
global models_ready, load_error
try:
# 백그라운드에서 모델 로드 시작
async def _runner():
global models_ready, load_error
try:
await load_models_async()
models_ready = True
load_error = None
except Exception as e:
logging.exception("Model loading failed")
models_ready = False
load_error = str(e)
asyncio.create_task(_runner())
except Exception as e:
logging.exception("Failed to schedule model loading")
models_ready = False
load_error = str(e)
yield
# 종료 시 정리 필요하면 여기서
app.router.lifespan_context = lifespan
# ========= 유틸 =========
def _build_features(daily: dict, date_obj: datetime) -> list[float]:
avg_temp = float(daily["temp"]["day"])
min_temp = float(daily["temp"]["min"])
max_temp = float(daily["temp"]["max"])
wind_speed = float(daily.get("wind_speed", 0.0))
humidity = float(daily.get("humidity", 0.0))
precipitation = float(daily.get("rain", 0.0)) # 없으면 0.0
year = date_obj.year
month = date_obj.month
day = date_obj.day
weekday = date_obj.weekday()
dayofyear = date_obj.timetuple().tm_yday
weekofyear = int(date_obj.strftime("%U"))
is_end_of_month = 1 if (date_obj + timedelta(days=1)).month != month else 0
kr_holidays = holidays.KR(years=[year])
is_holiday = 1 if date_obj.date() in kr_holidays else 0
is_monday_after_holiday = 1 if (weekday == 0 and (date_obj - timedelta(days=1)).date() in kr_holidays) else 0
season = 1 if month in [3, 4, 5] else 2 if month in [6, 7, 8] else 3 if month in [9, 10, 11] else 4
return [
avg_temp, min_temp, max_temp, precipitation, wind_speed, humidity,
year, month, day, weekday, dayofyear, weekofyear,
is_holiday, is_end_of_month, is_monday_after_holiday, season
]
# ========= 라우트 =========
@app.get("/health", tags=["health"])
def health():
"""App Service / AFD 헬스 프로브용 — 항상 가볍게 200"""
return {"status": "ok"}
@app.get("/ready", tags=["health"])
def ready():
"""모델 로드 준비 여부 확인용(선택)"""
return {"ready": models_ready, "error": load_error}
@app.get("/predictainer/predict")
def predict_8days():
# 모델 준비 안 됐으면 503
if not models_ready:
raise HTTPException(status_code=503, detail="Model is warming up. Try again in a moment.")
if not API_KEY:
raise HTTPException(status_code=500, detail="API_KEY is not configured on the server.")
# 1) 날씨 호출
try:
url = "https://api.openweathermap.org/data/3.0/onecall"
params = {
"lat": 34.901993,
"lon": 127.659044,
"units": "metric",
"lang": "kr",
"exclude": "current,minutely,hourly,alerts",
"appid": API_KEY,
}
resp = requests.get(url, params=params, timeout=6)
resp.raise_for_status()
weather_data = resp.json()
except requests.RequestException as e:
logging.warning(f"OpenWeather request failed: {e}")
raise HTTPException(status_code=502, detail="Weather API request failed")
daily_list = weather_data.get("daily", [])
if not daily_list:
raise HTTPException(status_code=502, detail="Weather API returned no daily data")
# 2) 예측
preds = []
for daily in daily_list:
date_obj = datetime.utcfromtimestamp(daily["dt"]) + timedelta(hours=9)
features = _build_features(daily, date_obj)
features_np = np.array([features], dtype=float)
# 스케일링은 MLP에만 적용(원 코드 유지)
scaled = scaler.transform(features_np)
try:
# LightGBM
pred_lgb = float(lgb_model.predict(features_np)[0])
# CatBoost
pred_cat = float(cat_model.predict(features_np)[0])
# TensorFlow
pred_mlp = float(mlp_model.predict(scaled, verbose=0)[0][0])
except Exception as e:
logging.exception("Prediction failed")
raise HTTPException(status_code=500, detail=f"Prediction failed: {e}")
ensemble = 0.6 * pred_lgb + 0.1 * pred_cat + 0.3 * pred_mlp
preds.append({
"date": date_obj.strftime("%Y-%m-%d"),
"weather": {
"avg_temp": daily["temp"]["day"],
"min_temp": daily["temp"]["min"],
"max_temp": daily["temp"]["max"],
"precipitation": daily.get("rain", 0.0),
"wind_speed": daily.get("wind_speed", 0.0),
"humidity": daily.get("humidity", 0.0),
},
"prediction": {"Ensemble": int(ensemble)}
})
return {"predictions": preds}