yokoha commited on
Commit
277a313
·
verified ·
1 Parent(s): 2a6cacd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +302 -52
app.py CHANGED
@@ -3,6 +3,7 @@ import pandas as pd
3
  import numpy as np
4
  from prophet import Prophet
5
  import plotly.express as px
 
6
  import matplotlib.pyplot as plt
7
  from datetime import date
8
  from pathlib import Path
@@ -12,9 +13,9 @@ import matplotlib as mpl
12
  # -------------------------------------------------
13
  # CONFIG ------------------------------------------
14
  # -------------------------------------------------
15
- CSV_PATH = Path("2025-domae.csv") # 파일 경로 수정
16
  MACRO_START, MACRO_END = "1996-01-01", "2030-12-31"
17
- MICRO_START, MICRO_END = "2020-01-01", "2026-12-31"
18
 
19
  # 한글 폰트 설정
20
  font_list = [f.name for f in fm.fontManager.ttflist if 'gothic' in f.name.lower() or
@@ -68,8 +69,12 @@ def _standardize_columns(df: pd.DataFrame) -> pd.DataFrame:
68
  if "date" in df.columns and pd.api.types.is_object_dtype(df["date"]):
69
  if len(df) > 0:
70
  sample = str(df["date"].iloc[0])
71
- if sample.isdigit() and len(sample) in (6, 8):
72
- df["date"] = pd.to_datetime(df["date"].astype(str).str[:6], format="%Y%m", errors="coerce")
 
 
 
 
73
 
74
  # ── build item from pdlt_nm + spcs_nm if needed ────────────────────
75
  if "item" not in df.columns and {"pdlt_nm", "spcs_nm"}.issubset(df.columns):
@@ -115,6 +120,9 @@ def load_data() -> pd.DataFrame:
115
  if before_date_convert != after_date_convert:
116
  st.warning(f"날짜 변환 중 {before_date_convert - after_date_convert}개 행이 제외되었습니다.")
117
 
 
 
 
118
  # NA 데이터 처리
119
  before_na_drop = len(df)
120
  df = df.dropna(subset=["date", "item", "price"])
@@ -146,13 +154,32 @@ def get_items(df: pd.DataFrame):
146
 
147
 
148
  @st.cache_data(show_spinner=False, ttl=3600)
149
- def fit_prophet(df: pd.DataFrame, horizon_end: str):
 
 
 
 
 
 
 
 
 
150
  # Make a copy and ensure we have data
151
  df = df.copy()
152
  df = df.dropna(subset=["date", "price"])
153
 
154
- # 중복 날짜 처리 - 동일 날짜에 여러 값이 있으면 평균값 사용
155
- df = df.groupby("date")["price"].mean().reset_index()
 
 
 
 
 
 
 
 
 
 
156
 
157
  if len(df) < 2:
158
  st.warning(f"데이터 포인트가 부족합니다. 예측을 위해서는 최소 2개 이상의 유효 데이터가 필요합니다. (현재 {len(df)}개)")
@@ -162,21 +189,52 @@ def fit_prophet(df: pd.DataFrame, horizon_end: str):
162
  prophet_df = df.rename(columns={"date": "ds", "price": "y"})
163
 
164
  try:
165
- # Fit the model
166
- m = Prophet(yearly_seasonality=True, weekly_seasonality=False, daily_seasonality=False)
 
 
 
 
 
 
 
 
 
 
 
167
  m.fit(prophet_df)
168
 
169
  # Generate future dates
170
- periods = max((pd.Timestamp(horizon_end) - df["date"].max()).days, 1)
171
- future = m.make_future_dataframe(periods=periods, freq="D")
 
 
 
 
 
 
 
 
172
 
173
  # Make predictions
174
  forecast = m.predict(future)
 
 
 
 
 
 
175
  return m, forecast
176
  except Exception as e:
177
  st.error(f"Prophet 모델 생성 중 오류: {str(e)}")
178
  return None, None
179
 
 
 
 
 
 
 
180
  # -------------------------------------------------
181
  # LOAD DATA ---------------------------------------
182
  # -------------------------------------------------
@@ -201,14 +259,12 @@ if item_df.empty:
201
  # -------------------------------------------------
202
  st.header(f"📈 {selected_item} 가격 예측 대시보드")
203
 
204
- # 데이터 필터링 로직 개선
205
  try:
206
- macro_start_dt = pd.Timestamp(MACRO_START)
207
- # 데이터가 충분하지 않으면 시작 날짜를 조정
208
- if len(item_df[item_df["date"] >= macro_start_dt]) < 10:
209
- # 가장 오래된 날짜부터 시작
210
  macro_start_dt = item_df["date"].min()
211
- st.info(f"충분한 데이터가 없어 시작 날짜를 {macro_start_dt.strftime('%Y-%m-%d')}로 조정했습니다.")
212
 
213
  macro_df = item_df[item_df["date"] >= macro_start_dt].copy()
214
  except Exception as e:
@@ -232,20 +288,89 @@ if len(macro_df) < 2:
232
  else:
233
  try:
234
  with st.spinner("장기 예측 모델 생성 중..."):
235
- m_macro, fc_macro = fit_prophet(macro_df, MACRO_END)
 
236
 
237
  if m_macro is not None and fc_macro is not None:
238
- fig_macro = px.line(fc_macro, x="ds", y="yhat", title="장기 예측 (1996–2030)")
239
- fig_macro.add_scatter(x=macro_df["date"], y=macro_df["price"], mode="lines", name="실제 가격")
240
- st.plotly_chart(fig_macro, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
- latest_price = macro_df.iloc[-1]["price"]
243
- # 2030년 마지막 날 찾기
244
- target_date = pd.Timestamp(MACRO_END)
245
- close_dates = fc_macro.loc[(fc_macro["ds"] - target_date).abs().argsort()[:1], "ds"].values[0]
246
- macro_pred = fc_macro.loc[fc_macro["ds"] == close_dates, "yhat"].iloc[0]
247
- macro_pct = (macro_pred - latest_price) / latest_price * 100
248
- st.metric("2030 예측가", f"{macro_pred:,.0f}", f"{macro_pct:+.1f}%")
 
 
 
 
 
 
 
 
 
249
  else:
250
  st.warning("예측 모델을 생성할 수 없습니다.")
251
  fig = px.line(item_df, x="date", y="price", title=f"{selected_item} 과거 가격")
@@ -258,44 +383,156 @@ else:
258
  # -------------------------------------------------
259
  # MICRO FORECAST 2024‑2026 ------------------------
260
  # -------------------------------------------------
261
- st.subheader("🔎 2024–2026 단기 예측")
262
 
263
- # 데이터 필터링 로직 개선
264
  try:
265
- micro_start_dt = pd.Timestamp(MICRO_START)
266
- # 데이터가 충분하지 않으면 시작 날짜를 조정
267
- if len(item_df[item_df["date"] >= micro_start_dt]) < 10:
268
- # 최근 30% 데이터만 사용
269
- n = max(2, int(len(item_df) * 0.3))
270
- micro_df = item_df.sort_values("date").tail(n).copy()
271
- st.info(f"충분한 최근 데이터가 없어 최근 {n}개 데이터 포인트만 사용합니다.")
272
- else:
273
- micro_df = item_df[item_df["date"] >= micro_start_dt].copy()
274
  except Exception as e:
275
  st.error(f"단기 예측 데이터 필터링 오류: {str(e)}")
276
- # 최근 10개 데이터 포인트 사용
277
- micro_df = item_df.sort_values("date").tail(10).copy()
278
 
279
  if len(micro_df) < 2:
280
- st.warning(f"{MICRO_START} 이후 데이터가 충분하지 않습니다.")
281
  fig = px.line(item_df, x="date", y="price", title=f"{selected_item} 최근 가격")
282
  st.plotly_chart(fig, use_container_width=True)
283
  else:
284
  try:
285
  with st.spinner("단기 예측 모델 생성 중..."):
286
- m_micro, fc_micro = fit_prophet(micro_df, MICRO_END)
 
287
 
288
  if m_micro is not None and fc_micro is not None:
289
- fig_micro = px.line(fc_micro, x="ds", y="yhat", title="단기 예측 (2024–2026)")
290
- fig_micro.add_scatter(x=micro_df["date"], y=micro_df["price"], mode="lines", name="실제 가격")
291
- st.plotly_chart(fig_micro, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
- latest_price = micro_df.iloc[-1]["price"]
294
- target_date = pd.Timestamp(MICRO_END)
295
- close_dates = fc_micro.loc[(fc_micro["ds"] - target_date).abs().argsort()[:1], "ds"].values[0]
296
- micro_pred = fc_micro.loc[fc_micro["ds"] == close_dates, "yhat"].iloc[0]
297
- micro_pct = (micro_pred - latest_price) / latest_price * 100
298
- st.metric("2026 예측가", f"{micro_pred:,.0f}", f"{micro_pct:+.1f}%")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  else:
300
  st.warning("단기 예측 모델을 생성할 수 없습니다.")
301
  except Exception as e:
@@ -317,6 +554,19 @@ with st.expander("📆 시즈널리티 & 패턴 설명"):
317
  f"**연간 피크 월:** {int(month_season.idxmax())}월 \n"
318
  f"**연간 저점 월:** {int(month_season.idxmin())}월 \n"
319
  f"**연간 변동폭:** {month_season.max() - month_season.min():.1f}")
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  except Exception as e:
321
  st.error(f"시즈널리티 분석 오류: {str(e)}")
322
  else:
 
3
  import numpy as np
4
  from prophet import Prophet
5
  import plotly.express as px
6
+ import plotly.graph_objects as go
7
  import matplotlib.pyplot as plt
8
  from datetime import date
9
  from pathlib import Path
 
13
  # -------------------------------------------------
14
  # CONFIG ------------------------------------------
15
  # -------------------------------------------------
16
+ CSV_PATH = Path("2025-domae.csv")
17
  MACRO_START, MACRO_END = "1996-01-01", "2030-12-31"
18
+ MICRO_START, MICRO_END = "2024-01-01", "2026-12-31"
19
 
20
  # 한글 폰트 설정
21
  font_list = [f.name for f in fm.fontManager.ttflist if 'gothic' in f.name.lower() or
 
69
  if "date" in df.columns and pd.api.types.is_object_dtype(df["date"]):
70
  if len(df) > 0:
71
  sample = str(df["date"].iloc[0])
72
+ if sample.isdigit() and len(sample) == 6: # YYYYMM 형식 확인
73
+ # 말일로 변환 (YYYYMM -> YYYY-MM-DD)
74
+ df["date"] = pd.to_datetime(df["date"].astype(str), format="%Y%m", errors="coerce")
75
+ df["date"] = df["date"] + pd.offsets.MonthEnd(0) # 해당 월의 마지막 날로 설정
76
+ elif sample.isdigit() and len(sample) == 8: # YYYYMMDD 형식
77
+ df["date"] = pd.to_datetime(df["date"].astype(str), format="%Y%m%d", errors="coerce")
78
 
79
  # ── build item from pdlt_nm + spcs_nm if needed ────────────────────
80
  if "item" not in df.columns and {"pdlt_nm", "spcs_nm"}.issubset(df.columns):
 
120
  if before_date_convert != after_date_convert:
121
  st.warning(f"날짜 변환 중 {before_date_convert - after_date_convert}개 행이 제외되었습니다.")
122
 
123
+ # 가격 데이터 정수형으로 변환 (숫자가 아닌 값 제거)
124
+ df["price"] = pd.to_numeric(df["price"], errors="coerce")
125
+
126
  # NA 데이터 처리
127
  before_na_drop = len(df)
128
  df = df.dropna(subset=["date", "item", "price"])
 
154
 
155
 
156
  @st.cache_data(show_spinner=False, ttl=3600)
157
+ def fit_prophet(df: pd.DataFrame, horizon_end: str, monthly=False, changepoint_prior_scale=0.05):
158
+ """
159
+ Prophet 모델을 학습시키고 예측합니다.
160
+
161
+ Args:
162
+ df: 학습 데이터 (date, price 컬럼 필요)
163
+ horizon_end: 예측 종료일
164
+ monthly: 월 단위 예측 여부
165
+ changepoint_prior_scale: 변화점 민감도 (낮을수록 과적합 감소)
166
+ """
167
  # Make a copy and ensure we have data
168
  df = df.copy()
169
  df = df.dropna(subset=["date", "price"])
170
 
171
+ # 이상치 제거 (99 퍼센타일 초과 가격 제외)
172
+ upper_limit = df["price"].quantile(0.99)
173
+ df = df[df["price"] <= upper_limit]
174
+
175
+ # 중복 날짜 처리
176
+ if monthly:
177
+ # 월 단위로 집계
178
+ df["year_month"] = df["date"].dt.strftime('%Y-%m')
179
+ df = df.groupby("year_month").agg({"date": "first", "price": "mean"}).reset_index(drop=True)
180
+ else:
181
+ # 일 단위로 집계
182
+ df = df.groupby("date")["price"].mean().reset_index()
183
 
184
  if len(df) < 2:
185
  st.warning(f"데이터 포인트가 부족합니다. 예측을 위해서는 최소 2개 이상의 유효 데이터가 필요합니다. (현재 {len(df)}개)")
 
189
  prophet_df = df.rename(columns={"date": "ds", "price": "y"})
190
 
191
  try:
192
+ # Fit the model with tuned parameters
193
+ m = Prophet(
194
+ yearly_seasonality=True,
195
+ weekly_seasonality=False,
196
+ daily_seasonality=False,
197
+ changepoint_prior_scale=changepoint_prior_scale, # 과적합 방지
198
+ seasonality_prior_scale=10.0, # 계절성 조정
199
+ seasonality_mode='multiplicative' # 곱셈 모드 (가격 데이터에 적합)
200
+ )
201
+
202
+ # 한국 명절 효과 추가 (설날, 추석)
203
+ m.add_country_holidays(country_name='South Korea')
204
+
205
  m.fit(prophet_df)
206
 
207
  # Generate future dates
208
+ if monthly:
209
+ # 단위 예측
210
+ future_periods = (pd.Timestamp(horizon_end).year - df["date"].max().year) * 12 + \
211
+ (pd.Timestamp(horizon_end).month - df["date"].max().month) + 1
212
+ future = m.make_future_dataframe(periods=future_periods, freq='MS') # 월 시작일
213
+ future = future.resample('MS', on='ds').first().reset_index() # 중복 제거
214
+ else:
215
+ # 일 단위 예측
216
+ periods = max((pd.Timestamp(horizon_end) - df["date"].max()).days, 1)
217
+ future = m.make_future_dataframe(periods=periods, freq="D")
218
 
219
  # Make predictions
220
  forecast = m.predict(future)
221
+
222
+ # 예측값 범위 조정 (음수 예측 방지 및 상한값 설정)
223
+ forecast['yhat'] = np.maximum(forecast['yhat'], 0) # 음수 제거
224
+ max_historical = prophet_df['y'].max() * 5 # 최대 역사적 가격의 5배로 제한
225
+ forecast['yhat'] = np.minimum(forecast['yhat'], max_historical) # 상한값 설정
226
+
227
  return m, forecast
228
  except Exception as e:
229
  st.error(f"Prophet 모델 생성 중 오류: {str(e)}")
230
  return None, None
231
 
232
+
233
+ def format_currency(value):
234
+ """원화 형식으로 숫자 포맷팅"""
235
+ return f"{value:,.0f}원"
236
+
237
+
238
  # -------------------------------------------------
239
  # LOAD DATA ---------------------------------------
240
  # -------------------------------------------------
 
259
  # -------------------------------------------------
260
  st.header(f"📈 {selected_item} 가격 예측 대시보드")
261
 
262
+ # 데이터 필터링 로직
263
  try:
264
+ macro_start_dt = pd.Timestamp("1996-01-01")
265
+ # 데이터의 시작일이 1996년 이후인지 확인
266
+ if item_df["date"].min() > macro_start_dt:
 
267
  macro_start_dt = item_df["date"].min()
 
268
 
269
  macro_df = item_df[item_df["date"] >= macro_start_dt].copy()
270
  except Exception as e:
 
288
  else:
289
  try:
290
  with st.spinner("장기 예측 모델 생성 중..."):
291
+ # 단위 예측으로 변경
292
+ m_macro, fc_macro = fit_prophet(macro_df, MACRO_END, monthly=True, changepoint_prior_scale=0.01)
293
 
294
  if m_macro is not None and fc_macro is not None:
295
+ # 실제 데이터와 예측 데이터 구분
296
+ cutoff_date = pd.Timestamp("2025-01-01")
297
+
298
+ # 플롯 생성
299
+ fig = go.Figure()
300
+
301
+ # 실제 데이터 추가 (1996-2024)
302
+ historical_data = macro_df[macro_df["date"] < cutoff_date].copy()
303
+ if not historical_data.empty:
304
+ fig.add_trace(go.Scatter(
305
+ x=historical_data["date"],
306
+ y=historical_data["price"],
307
+ mode="lines",
308
+ name="실제 가격 (1996-2024)",
309
+ line=dict(color="blue", width=2)
310
+ ))
311
+
312
+ # 예측 데이터 추가 (2025-2030)
313
+ forecast_data = fc_macro[fc_macro["ds"] >= cutoff_date].copy()
314
+ if not forecast_data.empty:
315
+ fig.add_trace(go.Scatter(
316
+ x=forecast_data["ds"],
317
+ y=forecast_data["yhat"],
318
+ mode="lines",
319
+ name="예측 가격 (2025-2030)",
320
+ line=dict(color="red", width=2, dash="dash")
321
+ ))
322
+
323
+ # 신뢰 구간 추가
324
+ fig.add_trace(go.Scatter(
325
+ x=forecast_data["ds"],
326
+ y=forecast_data["yhat_upper"],
327
+ mode="lines",
328
+ line=dict(width=0),
329
+ showlegend=False
330
+ ))
331
+ fig.add_trace(go.Scatter(
332
+ x=forecast_data["ds"],
333
+ y=forecast_data["yhat_lower"],
334
+ mode="lines",
335
+ line=dict(width=0),
336
+ fill="tonexty",
337
+ fillcolor="rgba(255, 0, 0, 0.1)",
338
+ name="95% 신뢰 구간"
339
+ ))
340
+
341
+ # 레이아웃 설정
342
+ fig.update_layout(
343
+ title=f"{selected_item} 장기 가격 예측 (1996-2030)",
344
+ xaxis_title="연도",
345
+ yaxis_title="가격 (원)",
346
+ legend=dict(
347
+ orientation="h",
348
+ yanchor="bottom",
349
+ y=1.02,
350
+ xanchor="right",
351
+ x=1
352
+ )
353
+ )
354
+
355
+ # 차트 표시
356
+ st.plotly_chart(fig, use_container_width=True)
357
 
358
+ # 2030년 예측가 표시
359
+ try:
360
+ latest_price = macro_df.iloc[-1]["price"]
361
+ # 2030년 마지막 찾기
362
+ target_date = pd.Timestamp("2030-12-31")
363
+ close_dates = fc_macro.loc[(fc_macro["ds"] - target_date).abs().argsort()[:1], "ds"].values[0]
364
+ macro_pred = fc_macro.loc[fc_macro["ds"] == close_dates, "yhat"].iloc[0]
365
+ macro_pct = (macro_pred - latest_price) / latest_price * 100
366
+
367
+ col1, col2 = st.columns(2)
368
+ with col1:
369
+ st.metric("현재 가격", format_currency(latest_price))
370
+ with col2:
371
+ st.metric("2030년 예측가", format_currency(macro_pred), f"{macro_pct:+.1f}%")
372
+ except Exception as e:
373
+ st.error(f"예측가 계산 오류: {str(e)}")
374
  else:
375
  st.warning("예측 모델을 생성할 수 없습니다.")
376
  fig = px.line(item_df, x="date", y="price", title=f"{selected_item} 과거 가격")
 
383
  # -------------------------------------------------
384
  # MICRO FORECAST 2024‑2026 ------------------------
385
  # -------------------------------------------------
386
+ st.subheader("🔎 2024–2026 단기 예측 (월별)")
387
 
388
+ # 데이터 필터링 - 최근 3년 데이터 활용
389
  try:
390
+ three_years_ago = pd.Timestamp("2021-01-01")
391
+ if item_df["date"].min() > three_years_ago:
392
+ three_years_ago = item_df["date"].min()
393
+
394
+ micro_df = item_df[item_df["date"] >= three_years_ago].copy()
 
 
 
 
395
  except Exception as e:
396
  st.error(f"단기 예측 데이터 필터링 오류: {str(e)}")
397
+ # 최근 데이터 사용
398
+ micro_df = item_df.sort_values("date").tail(24).copy()
399
 
400
  if len(micro_df) < 2:
401
+ st.warning(f"최근 데이터가 충분하지 않습니다.")
402
  fig = px.line(item_df, x="date", y="price", title=f"{selected_item} 최근 가격")
403
  st.plotly_chart(fig, use_container_width=True)
404
  else:
405
  try:
406
  with st.spinner("단기 예측 모델 생성 중..."):
407
+ # 단위 예측으로 변경
408
+ m_micro, fc_micro = fit_prophet(micro_df, MICRO_END, monthly=True, changepoint_prior_scale=0.05)
409
 
410
  if m_micro is not None and fc_micro is not None:
411
+ # 2024-01-01부터 2026-12-31까지 필터링
412
+ start_date = pd.Timestamp("2024-01-01")
413
+ end_date = pd.Timestamp("2026-12-31")
414
+
415
+ # 월별 데이터 준비
416
+ monthly_historical = micro_df.copy()
417
+ monthly_historical["year_month"] = monthly_historical["date"].dt.strftime("%Y-%m")
418
+ monthly_historical = monthly_historical.groupby("year_month").agg({
419
+ "date": "first",
420
+ "price": "mean"
421
+ }).reset_index(drop=True)
422
+
423
+ monthly_historical = monthly_historical[
424
+ (monthly_historical["date"] >= start_date) &
425
+ (monthly_historical["date"] <= end_date)
426
+ ]
427
+
428
+ monthly_forecast = fc_micro[
429
+ (fc_micro["ds"] >= start_date) &
430
+ (fc_micro["ds"] <= end_date)
431
+ ].copy()
432
+
433
+ # 월별 차트 생성
434
+ fig = go.Figure()
435
+
436
+ # 2024년 실제 데이터
437
+ actual_2024 = monthly_historical[
438
+ (monthly_historical["date"] >= pd.Timestamp("2024-01-01")) &
439
+ (monthly_historical["date"] <= pd.Timestamp("2024-12-31"))
440
+ ]
441
+
442
+ if not actual_2024.empty:
443
+ fig.add_trace(go.Scatter(
444
+ x=actual_2024["date"],
445
+ y=actual_2024["price"],
446
+ mode="lines+markers",
447
+ name="2024 실제 가격",
448
+ line=dict(color="blue", width=2),
449
+ marker=dict(size=8)
450
+ ))
451
+
452
+ # 2024년 이후 예측 데이터
453
+ cutoff = pd.Timestamp("2024-12-31")
454
+ future_data = monthly_forecast[monthly_forecast["ds"] > cutoff]
455
 
456
+ if not future_data.empty:
457
+ fig.add_trace(go.Scatter(
458
+ x=future_data["ds"],
459
+ y=future_data["yhat"],
460
+ mode="lines+markers",
461
+ name="2025-2026 예측 가격",
462
+ line=dict(color="red", width=2, dash="dash"),
463
+ marker=dict(size=8)
464
+ ))
465
+
466
+ # 신뢰 구간 추가
467
+ fig.add_trace(go.Scatter(
468
+ x=future_data["ds"],
469
+ y=future_data["yhat_upper"],
470
+ mode="lines",
471
+ line=dict(width=0),
472
+ showlegend=False
473
+ ))
474
+ fig.add_trace(go.Scatter(
475
+ x=future_data["ds"],
476
+ y=future_data["yhat_lower"],
477
+ mode="lines",
478
+ line=dict(width=0),
479
+ fill="tonexty",
480
+ fillcolor="rgba(255, 0, 0, 0.1)",
481
+ name="95% 신뢰 구간"
482
+ ))
483
+
484
+ # 레이아웃 설정
485
+ fig.update_layout(
486
+ title=f"{selected_item} 월별 단기 예측 (2024-2026)",
487
+ xaxis_title="월",
488
+ yaxis_title="가격 (원)",
489
+ xaxis=dict(
490
+ tickformat="%Y-%m",
491
+ dtick="M3", # 3개월 간격
492
+ tickangle=45
493
+ ),
494
+ legend=dict(
495
+ orientation="h",
496
+ yanchor="bottom",
497
+ y=1.02,
498
+ xanchor="right",
499
+ x=1
500
+ )
501
+ )
502
+
503
+ # 차트 표시
504
+ st.plotly_chart(fig, use_container_width=True)
505
+
506
+ # 월별 예측 가격 표시 (2025-2026)
507
+ with st.expander("월별 예측 가격 상세보기"):
508
+ monthly_detail = monthly_forecast[monthly_forecast["ds"] > cutoff].copy()
509
+ monthly_detail["날짜"] = monthly_detail["ds"].dt.strftime("%Y년 %m월")
510
+ monthly_detail["예측가격"] = monthly_detail["yhat"].apply(format_currency)
511
+ monthly_detail["하한값"] = monthly_detail["yhat_lower"].apply(format_currency)
512
+ monthly_detail["상한값"] = monthly_detail["yhat_upper"].apply(format_currency)
513
+
514
+ st.dataframe(
515
+ monthly_detail[["날짜", "예측가격", "하한값", "상한값"]],
516
+ hide_index=True
517
+ )
518
+
519
+ # 2026년 예측가 표시
520
+ try:
521
+ latest_price = monthly_historical.iloc[-1]["price"] if not monthly_historical.empty else micro_df.iloc[-1]["price"]
522
+
523
+ # 2026년 마지막 월 찾기
524
+ target_date = pd.Timestamp("2026-12-31")
525
+ close_dates = monthly_forecast.loc[(monthly_forecast["ds"] - target_date).abs().argsort()[:1], "ds"].values[0]
526
+ micro_pred = monthly_forecast.loc[monthly_forecast["ds"] == close_dates, "yhat"].iloc[0]
527
+ micro_pct = (micro_pred - latest_price) / latest_price * 100
528
+
529
+ col1, col2 = st.columns(2)
530
+ with col1:
531
+ st.metric("현재 가격", format_currency(latest_price))
532
+ with col2:
533
+ st.metric("2026년 12월 예측가", format_currency(micro_pred), f"{micro_pct:+.1f}%")
534
+ except Exception as e:
535
+ st.error(f"예측가 계산 오류: {str(e)}")
536
  else:
537
  st.warning("단기 예측 모델을 생성할 수 없습니다.")
538
  except Exception as e:
 
554
  f"**연간 피크 월:** {int(month_season.idxmax())}월 \n"
555
  f"**연간 저점 월:** {int(month_season.idxmin())}월 \n"
556
  f"**연간 변동폭:** {month_season.max() - month_season.min():.1f}")
557
+
558
+ # 월별 계절성 차트
559
+ month_names = ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"]
560
+ month_values = month_season.values
561
+
562
+ fig = px.bar(
563
+ x=month_names,
564
+ y=month_values,
565
+ title=f"{selected_item} 월별 가격 변동 패턴",
566
+ labels={"x": "월", "y": "상대적 가격 변동"}
567
+ )
568
+
569
+ st.plotly_chart(fig, use_container_width=True)
570
  except Exception as e:
571
  st.error(f"시즈널리티 분석 오류: {str(e)}")
572
  else: