yokoha commited on
Commit
7a4c9be
ยท
verified ยท
1 Parent(s): 4c5a48f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1050 -110
app.py CHANGED
@@ -1,14 +1,27 @@
1
  import streamlit as st
2
  import pandas as pd
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
10
  import matplotlib.font_manager as fm
11
  import matplotlib as mpl
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  # -------------------------------------------------
14
  # CONFIG ------------------------------------------
@@ -31,6 +44,77 @@ else:
31
 
32
  st.set_page_config(page_title="ํ’ˆ๋ชฉ๋ณ„ ๊ฐ€๊ฒฉ ์˜ˆ์ธก", page_icon="๐Ÿ“ˆ", layout="wide")
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  # -------------------------------------------------
35
  # UTILITIES ---------------------------------------
36
  # -------------------------------------------------
@@ -38,7 +122,6 @@ DATE_CANDIDATES = {"date", "ds", "ymd", "๋‚ ์งœ", "prce_reg_mm", "etl_ldg_dt"}
38
  ITEM_CANDIDATES = {"item", "ํ’ˆ๋ชฉ", "code", "category", "pdlt_nm", "spcs_nm"}
39
  PRICE_CANDIDATES = {"price", "y", "value", "๊ฐ€๊ฒฉ", "avrg_prce"}
40
 
41
-
42
  def _standardize_columns(df: pd.DataFrame) -> pd.DataFrame:
43
  """Standardize column names to date/item/price and deduplicate."""
44
  col_map = {}
@@ -87,7 +170,6 @@ def _standardize_columns(df: pd.DataFrame) -> pd.DataFrame:
87
 
88
  return df
89
 
90
-
91
  @st.cache_data(show_spinner=False)
92
  def load_data() -> pd.DataFrame:
93
  """Load price data from CSV file."""
@@ -142,102 +224,910 @@ def load_data() -> pd.DataFrame:
142
  return df
143
  except Exception as e:
144
  st.error(f"๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
145
- # ์˜ค๋ฅ˜ ์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ
146
  import traceback
147
  st.code(traceback.format_exc())
148
  st.stop()
149
 
150
-
151
  @st.cache_data(show_spinner=False)
152
  def get_items(df: pd.DataFrame):
153
  return sorted(df["item"].unique())
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)}๊ฐœ)")
186
- return None, None
 
 
 
 
187
 
188
- # Convert to Prophet format
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
  # -------------------------------------------------
 
241
  raw_df = load_data()
242
 
243
  if len(raw_df) == 0:
@@ -249,6 +1139,13 @@ selected_item = st.sidebar.selectbox("ํ’ˆ๋ชฉ", get_items(raw_df))
249
  current_date = date.today()
250
  st.sidebar.caption(f"์˜ค๋Š˜: {current_date}")
251
 
 
 
 
 
 
 
 
252
  item_df = raw_df.query("item == @selected_item").copy()
253
  if item_df.empty:
254
  st.error("์„ ํƒํ•œ ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ ์—†์Œ")
@@ -283,15 +1180,22 @@ with st.expander("๋ฐ์ดํ„ฐ ์ง„๋‹จ"):
283
 
284
  if len(macro_df) < 2:
285
  st.warning(f"{selected_item}์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ „์ฒด ๊ธฐ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.")
286
- fig = px.line(item_df, x="date", y="price", title=f"{selected_item} ๊ณผ๊ฑฐ ๊ฐ€๊ฒฉ")
 
 
287
  st.plotly_chart(fig, use_container_width=True)
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
 
@@ -309,8 +1213,10 @@ else:
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"],
@@ -338,6 +1244,9 @@ else:
338
  name="95% ์‹ ๋ขฐ ๊ตฌ๊ฐ„"
339
  ))
340
 
 
 
 
341
  # ๋ ˆ์ด์•„์›ƒ ์„ค์ •
342
  fig.update_layout(
343
  title=f"{selected_item} ์žฅ๊ธฐ ๊ฐ€๊ฒฉ ์˜ˆ์ธก (1996-2030)",
@@ -373,11 +1282,17 @@ else:
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} ๊ณผ๊ฑฐ ๊ฐ€๊ฒฉ")
 
 
377
  st.plotly_chart(fig, use_container_width=True)
378
  except Exception as e:
379
  st.error(f"์žฅ๊ธฐ ์˜ˆ์ธก ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
380
- fig = px.line(item_df, x="date", y="price", title=f"{selected_item} ๊ณผ๊ฑฐ ๊ฐ€๊ฒฉ")
 
 
 
 
381
  st.plotly_chart(fig, use_container_width=True)
382
 
383
  # -------------------------------------------------
@@ -399,15 +1314,19 @@ except Exception as e:
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")
@@ -481,6 +1400,9 @@ else:
481
  name="95% ์‹ ๋ขฐ ๊ตฌ๊ฐ„"
482
  ))
483
 
 
 
 
484
  # ๋ ˆ์ด์•„์›ƒ ์„ค์ •
485
  fig.update_layout(
486
  title=f"{selected_item} ์›”๋ณ„ ๋‹จ๊ธฐ ์˜ˆ์ธก (2024-2026)",
@@ -541,36 +1463,54 @@ else:
541
  # -------------------------------------------------
542
  # SEASONALITY & PATTERN ---------------------------
543
  # -------------------------------------------------
544
- with st.expander("๐Ÿ“† ์‹œ์ฆˆ๋„๋ฆฌํ‹ฐ & ํŒจํ„ด ์„ค๋ช…"):
545
- if 'm_micro' in locals() and m_micro is not None and 'fc_micro' in locals() and fc_micro is not None:
546
  try:
547
- comp_fig = m_micro.plot_components(fc_micro)
548
- st.pyplot(comp_fig)
549
-
550
- month_season = (fc_micro[["ds", "yearly"]]
551
- .assign(month=lambda d: d.ds.dt.month)
552
- .groupby("month")["yearly"].mean())
553
- st.markdown(
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:
573
- st.info("ํŒจํ„ด ๋ถ„์„์„ ์œ„ํ•œ ์ถฉ๋ถ„ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
574
 
575
  # -------------------------------------------------
576
  # FOOTER ------------------------------------------
 
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
 
 
 
4
  import matplotlib.pyplot as plt
5
+ import plotly.graph_objects as go
6
  from datetime import date
7
  from pathlib import Path
8
  import matplotlib.font_manager as fm
9
  import matplotlib as mpl
10
+ import warnings
11
+ warnings.filterwarnings('ignore')
12
+
13
+ # ํ•„์š”ํ•œ ์ถ”๊ฐ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋กœ๋“œ
14
+ try:
15
+ import statsmodels.api as sm
16
+ from statsmodels.tsa.statespace.sarimax import SARIMAX
17
+ from statsmodels.tsa.holtwinters import ExponentialSmoothing, SimpleExpSmoothing, Holt
18
+ from statsmodels.tsa.seasonal import seasonal_decompose
19
+ from sklearn.linear_model import LinearRegression
20
+ from sklearn.metrics import mean_absolute_percentage_error
21
+ except ImportError:
22
+ st.error("ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ„ฐ๋ฏธ๋„์—์„œ ๋‹ค์Œ ๋ช…๋ น์„ ์‹คํ–‰ํ•˜์„ธ์š”:")
23
+ st.code("pip install statsmodels scikit-learn")
24
+ st.stop()
25
 
26
  # -------------------------------------------------
27
  # CONFIG ------------------------------------------
 
44
 
45
  st.set_page_config(page_title="ํ’ˆ๋ชฉ๋ณ„ ๊ฐ€๊ฒฉ ์˜ˆ์ธก", page_icon="๐Ÿ“ˆ", layout="wide")
46
 
47
+ # -------------------------------------------------
48
+ # ํ’ˆ๋ชฉ๋ณ„ ์ตœ์  ๋ชจ๋ธ ๋งคํ•‘ ---------------------------
49
+ # -------------------------------------------------
50
+ item_models = {
51
+ "๊ฐˆ์น˜": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.82, "model2": "Holt-Winters", "accuracy2": 99.80},
52
+ "๊ฐ์ž": {"model1": "ETS(Multiplicative)", "accuracy1": 99.58, "model2": "SARIMA(1,0,1)(1,0,1,12)", "accuracy2": 98.70},
53
+ "๊ฑด๊ณ ์ถ”": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.96, "model2": "Holt", "accuracy2": 99.79},
54
+ "๊ฑด๋‹ค์‹œ๋งˆ": {"model1": "Naive", "accuracy1": 99.59, "model2": "SeasonalNaive", "accuracy2": 99.34},
55
+ "๊ณ ๊ตฌ๋งˆ": {"model1": "SARIMA(1,1,1)(1,1,1,12)", "accuracy1": 99.89, "model2": "ETS(Multiplicative)", "accuracy2": 98.91},
56
+ "๊ณ ๋“ฑ์–ด": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.48, "model2": "ETS(Additive)", "accuracy2": 99.42},
57
+ "๊น€": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.99, "model2": "SARIMA(0,1,1)(0,1,1,12)", "accuracy2": 99.93},
58
+ "๊น๋งˆ๋Š˜(๊ตญ์‚ฐ)": {"model1": "SeasonalNaive", "accuracy1": 99.79, "model2": "MovingAverage-6 m", "accuracy2": 98.65},
59
+ "๊นป์žŽ": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.68, "model2": "Holt", "accuracy2": 99.54},
60
+ "๋…น๋‘": {"model1": "WeightedMA-6 m", "accuracy1": 99.53, "model2": "Fourier + LR", "accuracy2": 99.53},
61
+ "๋Аํƒ€๋ฆฌ๋ฒ„์„ฏ": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.84, "model2": "LinearTrend", "accuracy2": 99.80},
62
+ "๋‹น๊ทผ": {"model1": "Holt", "accuracy1": 99.25, "model2": "ETS(Multiplicative)", "accuracy2": 97.27},
63
+ "๋“ค๊นจ": {"model1": "Holt", "accuracy1": 99.57, "model2": "Holt-Winters", "accuracy2": 99.17},
64
+ "๋•…์ฝฉ": {"model1": "SARIMA(1,1,1)(1,1,1,12)", "accuracy1": 99.74, "model2": "ETS(Additive)", "accuracy2": 99.37},
65
+ "๋ ˆ๋ชฌ": {"model1": "WeightedMA-6 m", "accuracy1": 99.99, "model2": "LinearTrend", "accuracy2": 98.99},
66
+ "๋ง๊ณ ": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.38, "model2": "Holt-Winters", "accuracy2": 99.02},
67
+ "๋ฉ”๋ฐ€": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.48, "model2": "SARIMA(0,1,1)(0,1,1,12)", "accuracy2": 98.99},
68
+ "๋ฉœ๋ก ": {"model1": "Naive", "accuracy1": 99.07, "model2": "ETS(Multiplicative)", "accuracy2": 99.01},
69
+ "๋ช…ํƒœ": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 100.00, "model2": "MovingAverage-6 m", "accuracy2": 99.93},
70
+ "๋ฌด": {"model1": "SARIMA(1,1,1)(1,1,1,12)", "accuracy1": 99.54, "model2": "SeasonalNaive", "accuracy2": 88.29, "special": "accuracy_drop"},
71
+ "๋ฌผ์˜ค์ง•์–ด": {"model1": "Holt-Winters", "accuracy1": 99.91, "model2": "ETS(Multiplicative)", "accuracy2": 99.36},
72
+ "๋ฏธ๋‚˜๋ฆฌ": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 98.71, "model2": "LinearTrend", "accuracy2": 98.54},
73
+ "๋ฐ”๋‚˜๋‚˜": {"model1": "MovingAverage-6 m", "accuracy1": 99.81, "model2": "ETS(Multiplicative)", "accuracy2": 98.86},
74
+ "๋ฐฉ์šธํ† ๋งˆํ† ": {"model1": "ETS(Multiplicative)", "accuracy1": 99.62, "model2": "Holt", "accuracy2": 98.28},
75
+ "๋ฐฐ": {"model1": "ETS(Additive)", "accuracy1": 99.34, "model2": "LinearTrend", "accuracy2": 98.57},
76
+ "๋ฐฐ์ถ”": {"model1": "Holt", "accuracy1": 99.98, "model2": "MovingAverage-6 m", "accuracy2": 99.71},
77
+ "๋ถ์–ด": {"model1": "Fourier + LR", "accuracy1": 99.96, "model2": "MovingAverage-6 m", "accuracy2": 99.94},
78
+ "๋ถ‰์€๊ณ ์ถ”": {"model1": "SARIMA(1,1,1)(1,1,1,12)", "accuracy1": 99.75, "model2": "LinearTrend", "accuracy2": 97.61},
79
+ "๋ธŒ๋กœ์ฝœ๋ฆฌ": {"model1": "Holt", "accuracy1": 99.54, "model2": "Naive", "accuracy2": 99.93},
80
+ "์‚ฌ๊ณผ": {"model1": "Holt-Winters", "accuracy1": 99.89, "model2": "ETS(Multiplicative)", "accuracy2": 98.91},
81
+ "์ƒ์ถ”": {"model1": "ETS(Additive)", "accuracy1": 99.11, "model2": "Holt-Winters", "accuracy2": 97.61},
82
+ "์ƒˆ์†ก์ด๋ฒ„์„ฏ": {"model1": "SimpleExpSmoothing", "accuracy1": 99.95, "model2": "Holt-Winters", "accuracy2": 99.40},
83
+ "์ƒˆ์šฐ": {"model1": "ETS(Additive)", "accuracy1": 99.87, "model2": "Naive", "accuracy2": 99.96},
84
+ "์ƒ๊ฐ•": {"model1": "Naive", "accuracy1": 99.27, "model2": "ETS(Additive)", "accuracy2": 98.53},
85
+ "์ˆ˜๋ฐ•": {"model1": "Naive", "accuracy1": 99.91, "model2": "SARIMA(1,1,1)(1,1,1,12)", "accuracy2": 99.45},
86
+ "์‹œ๊ธˆ์น˜": {"model1": "Holt-Winters", "accuracy1": 99.70, "model2": "SeasonalNaive", "accuracy2": 98.73},
87
+ "์Œ€": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.99, "model2": "Holt-Winters", "accuracy2": 99.88},
88
+ "์•Œ๋ฐฐ๊ธฐ๋ฐฐ์ถ”": {"model1": "WeightedMA-6 m", "accuracy1": 98.19, "model2": "SeasonalNaive", "accuracy2": 95.73},
89
+ "์–‘๋ฐฐ์ถ”": {"model1": "Holt-Winters", "accuracy1": 99.05, "model2": "WeightedMA-6 m", "accuracy2": 97.85},
90
+ "์–‘ํŒŒ": {"model1": "ETS(Additive)", "accuracy1": 99.93, "model2": "WeightedMA-6 m", "accuracy2": 99.51},
91
+ "์–ผ๊ฐˆ์ด๋ฐฐ์ถ”": {"model1": "SARIMA(1,1,1)(1,1,1,12)", "accuracy1": 99.77, "model2": "SeasonalNaive", "accuracy2": 98.55},
92
+ "์—ด๋ฌด": {"model1": "SeasonalNaive", "accuracy1": 99.96, "model2": "Holt", "accuracy2": 99.50},
93
+ "์˜ค์ด": {"model1": "SeasonalNaive", "accuracy1": 99.82, "model2": "ETS(Additive)", "accuracy2": 98.48},
94
+ "์ „๋ณต": {"model1": "Holt", "accuracy1": 99.90, "model2": "Fourier + LR", "accuracy2": 99.90},
95
+ "์ฐธ๊นจ": {"model1": "WeightedMA-6 m", "accuracy1": 100.00, "model2": "LinearTrend", "accuracy2": 86.44, "special": "accuracy_drop"},
96
+ "์ฐน์Œ€": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.71, "model2": "Naive", "accuracy2": 98.64, "special": "accuracy_drop"},
97
+ "์ฝฉ": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.98, "model2": "ETS(Additive)", "accuracy2": 99.68},
98
+ "ํ† ๋งˆํ† ": {"model1": "SeasonalNaive", "accuracy1": 97.31, "model2": "MovingAverage-6 m", "accuracy2": 97.57},
99
+ "ํŒŒ": {"model1": "MovingAverage-6 m", "accuracy1": 99.92, "model2": "Holt-Winters", "accuracy2": 97.77},
100
+ "ํŒŒ์ธ์• ํ”Œ": {"model1": "Naive", "accuracy1": 99.51, "model2": "SARIMA(1,0,1)(1,0,1,12)", "accuracy2": 96.39},
101
+ "ํŒŒํ”„๋ฆฌ์นด": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.04, "model2": "WeightedMA-6 m", "accuracy2": 99.36},
102
+ "ํŒฅ": {"model1": "ETS(Additive)", "accuracy1": 99.87, "model2": "Holt-Winters", "accuracy2": 75.08, "special": "accuracy_drop"},
103
+ "ํŒฝ์ด๋ฒ„์„ฏ": {"model1": "SeasonalNaive", "accuracy1": 99.84, "model2": "Fourier + LR", "accuracy2": 98.49},
104
+ "ํ’‹๊ณ ์ถ”": {"model1": "Holt-Winters", "accuracy1": 98.95, "model2": "ETS(Multiplicative)", "accuracy2": 98.73},
105
+ "ํ”ผ๋ง": {"model1": "Fourier + LR", "accuracy1": 99.64, "model2": "WeightedMA-6 m", "accuracy2": 98.93},
106
+ "ํ˜ธ๋ฐ•": {"model1": "ETS(Multiplicative)", "accuracy1": 99.98, "model2": "SeasonalNaive", "accuracy2": 96.61},
107
+ "ํ™ํ•ฉ": {"model1": "Naive", "accuracy1": 99.86, "model2": "SeasonalNaive", "accuracy2": 98.56},
108
+ }
109
+
110
+ # ๊ธฐํƒ€ ํ’ˆ๋ชฉ์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ๋ชจ๋ธ (๋ฆฌ์ŠคํŠธ์— ์—†๋Š” ํ’ˆ๋ชฉ)
111
+ default_models = {
112
+ "model1": "SARIMA(1,0,1)(1,0,1,12)",
113
+ "accuracy1": 99.0,
114
+ "model2": "ETS(Multiplicative)",
115
+ "accuracy2": 98.0
116
+ }
117
+
118
  # -------------------------------------------------
119
  # UTILITIES ---------------------------------------
120
  # -------------------------------------------------
 
122
  ITEM_CANDIDATES = {"item", "ํ’ˆ๋ชฉ", "code", "category", "pdlt_nm", "spcs_nm"}
123
  PRICE_CANDIDATES = {"price", "y", "value", "๊ฐ€๊ฒฉ", "avrg_prce"}
124
 
 
125
  def _standardize_columns(df: pd.DataFrame) -> pd.DataFrame:
126
  """Standardize column names to date/item/price and deduplicate."""
127
  col_map = {}
 
170
 
171
  return df
172
 
 
173
  @st.cache_data(show_spinner=False)
174
  def load_data() -> pd.DataFrame:
175
  """Load price data from CSV file."""
 
224
  return df
225
  except Exception as e:
226
  st.error(f"๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
 
227
  import traceback
228
  st.code(traceback.format_exc())
229
  st.stop()
230
 
 
231
  @st.cache_data(show_spinner=False)
232
  def get_items(df: pd.DataFrame):
233
  return sorted(df["item"].unique())
234
 
235
+ def get_best_model_for_item(item):
236
+ """ํ’ˆ๋ชฉ์— ๋งž๋Š” ์ตœ์  ๋ชจ๋ธ ์ •๋ณด ๋ฐ˜ํ™˜"""
237
+ return item_models.get(item, default_models)
238
+
239
+ def format_currency(value):
240
+ """์›ํ™” ํ˜•์‹์œผ๋กœ ์ˆซ์ž ํฌ๋งทํŒ…"""
241
+ if pd.isna(value) or not np.isfinite(value):
242
+ return "N/A"
243
+ return f"{value:,.0f}์›"
244
 
245
+ # -------------------------------------------------
246
+ # ๋ชจ๋ธ ๊ตฌํ˜„๋ถ€ --------------------------------------
247
+ # -------------------------------------------------
248
  @st.cache_data(show_spinner=False, ttl=3600)
249
+ def prepare_monthly_data(df):
250
+ """์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„"""
251
+ # ์›”๋ณ„๋กœ ์ง‘๊ณ„
252
+ monthly_df = df.copy()
253
+ monthly_df['year_month'] = monthly_df['date'].dt.strftime('%Y-%m')
254
+ monthly_df = monthly_df.groupby('year_month').agg({'date': 'last', 'price': 'mean'}).reset_index(drop=True)
255
+ monthly_df.sort_values('date', inplace=True)
 
 
 
 
 
 
256
 
257
+ # ์ธ๋ฑ์Šค ์„ค์ •
258
+ monthly_df.set_index('date', inplace=True)
 
259
 
260
+ # ๊ฒฐ์ธก์น˜ ๋ณด๊ฐ„ (์›”๋ณ„ ๋ฐ์ดํ„ฐ์— ๋นˆ ์›”์ด ์žˆ์„ ์ˆ˜ ์žˆ์Œ)
261
+ if len(monthly_df) > 1:
262
+ monthly_df = monthly_df.asfreq('M', method='ffill')
 
 
 
 
 
263
 
264
+ return monthly_df
265
+
266
+ def fit_sarima(df, order, seasonal_order, horizon_end):
267
+ """SARIMA ๋ชจ๋ธ ๊ตฌํ˜„"""
268
+ import pandas as pd
269
+ import numpy as np
270
+ from statsmodels.tsa.statespace.sarimax import SARIMAX
271
 
272
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
273
+ monthly_df = prepare_monthly_data(df)
274
 
275
+ # ๋ชจ๋ธ ํ•™์Šต
276
  try:
277
+ model = SARIMAX(
278
+ monthly_df['price'],
279
+ order=order,
280
+ seasonal_order=seasonal_order,
281
+ enforce_stationarity=False,
282
+ enforce_invertibility=False
 
 
283
  )
284
+ results = model.fit(disp=False)
285
 
286
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
287
+ last_date = monthly_df.index[-1]
288
+ end_date = pd.Timestamp(horizon_end)
289
+ periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
290
 
291
+ # ์˜ˆ์ธก ์ˆ˜ํ–‰
292
+ forecast = results.get_forecast(steps=periods)
293
+ pred_mean = forecast.predicted_mean
294
+ pred_ci = forecast.conf_int()
295
 
296
+ # Prophet ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
297
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
298
+
299
+ fc_df = pd.DataFrame({
300
+ 'ds': future_dates,
301
+ 'yhat': pred_mean.values,
302
+ 'yhat_lower': pred_ci.iloc[:, 0].values,
303
+ 'yhat_upper': pred_ci.iloc[:, 1].values
304
+ })
 
 
305
 
306
+ # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜ (๋‚ ์งœ, ๊ฐ€๊ฒฉ)
307
+ fc_df_monthly = pd.DataFrame({
308
+ 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
309
+ })
310
 
311
+ # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๏ฟฝ๏ฟฝ๊ณผ ์ถ”๊ฐ€
312
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
313
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
314
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
315
 
316
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
317
+ fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
318
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
319
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
320
+
321
+ # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
322
+ fc_df_monthly['yearly'] = 0
323
+ fc_df_monthly['trend'] = 0
324
+
325
+ try:
326
+ # ๊ฐ€๋Šฅํ•˜๋ฉด ๊ณ„์ ˆ์„ฑ ๋ถ„ํ•ด
327
+ decomposition = seasonal_decompose(monthly_df['price'], model='multiplicative', period=12)
328
+ trend = decomposition.trend
329
+ seasonal = decomposition.seasonal
330
+
331
+ # ๊ฒฐ๊ณผ์— ๊ณ„์ ˆ์„ฑ ๋ฐ˜์˜
332
+ for i, date in enumerate(fc_df_monthly['ds']):
333
+ month = date.month
334
+ if month in seasonal.index.month:
335
+ seasonal_value = seasonal[seasonal.index.month == month].mean()
336
+ fc_df_monthly.loc[i, 'yearly'] = seasonal_value
337
+ except:
338
+ pass
339
+
340
+ return fc_df_monthly
341
+
342
  except Exception as e:
343
+ st.error(f"SARIMA ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
344
+ return None
345
 
346
+ def fit_ets(df, seasonal_type, horizon_end):
347
+ """ETS ๋ชจ๋ธ ๊ตฌํ˜„"""
348
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
349
+ monthly_df = prepare_monthly_data(df)
350
+
351
+ # ๋ชจ๋ธ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
352
+ if seasonal_type == 'multiplicative':
353
+ trend_type = 'add'
354
+ seasonal = 'mul'
355
+ else: # additive
356
+ trend_type = 'add'
357
+ seasonal = 'add'
358
+
359
+ # ๋ชจ๋ธ ํ•™์Šต
360
+ try:
361
+ model = ExponentialSmoothing(
362
+ monthly_df['price'],
363
+ trend=trend_type,
364
+ seasonal=seasonal,
365
+ seasonal_periods=12,
366
+ damped=True
367
+ )
368
+ results = model.fit(optimized=True)
369
+
370
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
371
+ last_date = monthly_df.index[-1]
372
+ end_date = pd.Timestamp(horizon_end)
373
+ periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
374
+
375
+ # ์˜ˆ์ธก ์ˆ˜ํ–‰
376
+ forecast = results.forecast(periods)
377
+
378
+ # Prophet ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
379
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
380
+
381
+ # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ • (ETS๋Š” ๊ธฐ๋ณธ ์‹ ๋ขฐ ๊ตฌ๊ฐ„์„ ์ œ๊ณตํ•˜์ง€ ์•Š์Œ)
382
+ std_error = np.std(results.resid)
383
+ lower_bound = forecast - 1.96 * std_error
384
+ upper_bound = forecast + 1.96 * std_error
385
+
386
+ fc_df = pd.DataFrame({
387
+ 'ds': future_dates,
388
+ 'yhat': forecast.values,
389
+ 'yhat_lower': lower_bound.values,
390
+ 'yhat_upper': upper_bound.values
391
+ })
392
+
393
+ # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
394
+ fc_df_monthly = pd.DataFrame({
395
+ 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
396
+ })
397
+
398
+ # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
399
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
400
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
401
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
402
+
403
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
404
+ fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
405
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
406
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
407
+
408
+ # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
409
+ fc_df_monthly['yearly'] = 0
410
+ fc_df_monthly['trend'] = 0
411
+
412
+ try:
413
+ # ๊ฐ€๋Šฅํ•˜๋ฉด ๊ณ„์ ˆ์„ฑ ๋ถ„ํ•ด
414
+ decomposition = seasonal_decompose(monthly_df['price'], model=seasonal_type, period=12)
415
+ trend = decomposition.trend
416
+ seasonal = decomposition.seasonal
417
+
418
+ # ๊ฒฐ๊ณผ์— ๊ณ„์ ˆ์„ฑ ๋ฐ˜์˜
419
+ for i, date in enumerate(fc_df_monthly['ds']):
420
+ month = date.month
421
+ if month in seasonal.index.month:
422
+ seasonal_value = seasonal[seasonal.index.month == month].mean()
423
+ fc_df_monthly.loc[i, 'yearly'] = seasonal_value
424
+ except:
425
+ pass
426
+
427
+ return fc_df_monthly
428
+
429
+ except Exception as e:
430
+ st.error(f"ETS ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
431
+ return None
432
 
433
+ def fit_holt(df, horizon_end):
434
+ """Holt ๋ชจ๋ธ ๊ตฌํ˜„"""
435
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
436
+ monthly_df = prepare_monthly_data(df)
437
+
438
+ # ๋ชจ๋ธ ํ•™์Šต
439
+ try:
440
+ model = Holt(monthly_df['price'], damped=True)
441
+ results = model.fit(optimized=True)
442
+
443
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
444
+ last_date = monthly_df.index[-1]
445
+ end_date = pd.Timestamp(horizon_end)
446
+ periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
447
+
448
+ # ์˜ˆ์ธก ์ˆ˜ํ–‰
449
+ forecast = results.forecast(periods)
450
+
451
+ # Prophet ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
452
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
453
+
454
+ # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
455
+ std_error = np.std(results.resid)
456
+ lower_bound = forecast - 1.96 * std_error
457
+ upper_bound = forecast + 1.96 * std_error
458
+
459
+ fc_df = pd.DataFrame({
460
+ 'ds': future_dates,
461
+ 'yhat': forecast.values,
462
+ 'yhat_lower': lower_bound.values,
463
+ 'yhat_upper': upper_bound.values
464
+ })
465
+
466
+ # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
467
+ fc_df_monthly = pd.DataFrame({
468
+ 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
469
+ })
470
+
471
+ # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
472
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
473
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
474
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
475
+
476
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
477
+ fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
478
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
479
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
480
+
481
+ # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
482
+ fc_df_monthly['yearly'] = 0
483
+ fc_df_monthly['trend'] = fc_df_monthly['yhat'] # Holt๋Š” ์ถ”์„ธ๋งŒ ๋ชจ๋ธ๋ง
484
+
485
+ return fc_df_monthly
486
+
487
+ except Exception as e:
488
+ st.error(f"Holt ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
489
+ return None
490
+
491
+ def fit_holt_winters(df, horizon_end):
492
+ """Holt-Winters ๋ชจ๋ธ ๊ตฌํ˜„"""
493
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
494
+ monthly_df = prepare_monthly_data(df)
495
+
496
+ # ๋ชจ๋ธ ํ•™์Šต
497
+ try:
498
+ model = ExponentialSmoothing(
499
+ monthly_df['price'],
500
+ trend='add',
501
+ seasonal='mul', # ๊ณ„์ ˆ์„ฑ์€ ๊ณฑ์…ˆ ๋ฐฉ์‹์ด ๋†์‚ฐ๋ฌผ ๊ฐ€๊ฒฉ์— ๋” ์ ํ•ฉ
502
+ seasonal_periods=12,
503
+ damped=True
504
+ )
505
+ results = model.fit(optimized=True)
506
+
507
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
508
+ last_date = monthly_df.index[-1]
509
+ end_date = pd.Timestamp(horizon_end)
510
+ periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
511
+
512
+ # ์˜ˆ์ธก ์ˆ˜ํ–‰
513
+ forecast = results.forecast(periods)
514
+
515
+ # Prophet ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
516
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
517
+
518
+ # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
519
+ std_error = np.std(results.resid)
520
+ lower_bound = forecast - 1.96 * std_error
521
+ upper_bound = forecast + 1.96 * std_error
522
+
523
+ fc_df = pd.DataFrame({
524
+ 'ds': future_dates,
525
+ 'yhat': forecast.values,
526
+ 'yhat_lower': lower_bound.values,
527
+ 'yhat_upper': upper_bound.values
528
+ })
529
+
530
+ # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
531
+ fc_df_monthly = pd.DataFrame({
532
+ 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
533
+ })
534
+
535
+ # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
536
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
537
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
538
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
539
+
540
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
541
+ fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
542
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
543
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
544
+
545
+ # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
546
+ fc_df_monthly['yearly'] = 0
547
+ fc_df_monthly['trend'] = 0
548
+
549
+ try:
550
+ # Holt-Winters ๋ชจ๋ธ์—์„œ ๊ณ„์ ˆ์„ฑ ์ถ”์ถœ
551
+ seasonal = results.seasonal_
552
+
553
+ # ๊ฒฐ๊ณผ์— ๊ณ„์ ˆ์„ฑ ๋ฐ˜์˜
554
+ for i, date in enumerate(fc_df_monthly['ds']):
555
+ month = date.month - 1 # 0-indexed
556
+ if month < len(seasonal):
557
+ fc_df_monthly.loc[i, 'yearly'] = seasonal[month] * fc_df_monthly.loc[i, 'yhat']
558
+ fc_df_monthly.loc[i, 'trend'] = fc_df_monthly.loc[i, 'yhat'] - fc_df_monthly.loc[i, 'yearly']
559
+ except:
560
+ pass
561
+
562
+ return fc_df_monthly
563
+
564
+ except Exception as e:
565
+ st.error(f"Holt-Winters ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
566
+ return None
567
+
568
+ def fit_moving_average(df, window, horizon_end):
569
+ """์ด๋™ ํ‰๊ท  ๋ชจ๋ธ ๊ตฌํ˜„"""
570
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
571
+ monthly_df = prepare_monthly_data(df)
572
+
573
+ try:
574
+ # ๋งˆ์ง€๋ง‰ window ๊ฐœ์›”์˜ ํ‰๊ท  ๊ณ„์‚ฐ
575
+ last_values = monthly_df['price'].iloc[-window:]
576
+ ma_value = last_values.mean()
577
+
578
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
579
+ last_date = monthly_df.index[-1]
580
+ end_date = pd.Timestamp(horizon_end)
581
+ periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
582
+
583
+ # ์˜ˆ์ธก ์ˆ˜ํ–‰ (๋ชจ๋“  ๋ฏธ๋ž˜ ์‹œ์ ์— ๋™์ผํ•œ ๊ฐ’)
584
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
585
+
586
+ # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
587
+ std_error = last_values.std()
588
+ lower_bound = ma_value - 1.96 * std_error
589
+ upper_bound = ma_value + 1.96 * std_error
590
+
591
+ fc_df = pd.DataFrame({
592
+ 'ds': future_dates,
593
+ 'yhat': [ma_value] * len(future_dates),
594
+ 'yhat_lower': [lower_bound] * len(future_dates),
595
+ 'yhat_upper': [upper_bound] * len(future_dates)
596
+ })
597
+
598
+ # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
599
+ fc_df_monthly = pd.DataFrame({
600
+ 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
601
+ })
602
+
603
+ # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
604
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
605
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
606
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
607
+
608
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
609
+ fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
610
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
611
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
612
+
613
+ # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
614
+ fc_df_monthly['yearly'] = 0
615
+ fc_df_monthly['trend'] = fc_df_monthly['yhat']
616
+
617
+ return fc_df_monthly
618
+
619
+ except Exception as e:
620
+ st.error(f"์ด๋™ ํ‰๊ท  ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
621
+ return None
622
+
623
+ def fit_weighted_ma(df, window, horizon_end):
624
+ """๊ฐ€์ค‘ ์ด๋™ ํ‰๊ท  ๋ชจ๋ธ ๊ตฌํ˜„"""
625
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
626
+ monthly_df = prepare_monthly_data(df)
627
+
628
+ try:
629
+ # ๋งˆ์ง€๋ง‰ window ๊ฐœ์›”์˜ ๊ฐ€์ค‘ ํ‰๊ท  ๊ณ„์‚ฐ
630
+ last_values = monthly_df['price'].iloc[-window:].to_numpy()
631
+
632
+ # ๊ฐ€์ค‘์น˜ ์ƒ์„ฑ (์ตœ๊ทผ ๋ฐ์ดํ„ฐ์— ๋” ๋†’์€ ๊ฐ€์ค‘์น˜)
633
+ weights = np.arange(1, window + 1)
634
+ weights = weights / np.sum(weights)
635
+
636
+ wma_value = np.sum(last_values * weights)
637
+
638
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
639
+ last_date = monthly_df.index[-1]
640
+ end_date = pd.Timestamp(horizon_end)
641
+ periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
642
+
643
+ # ์˜ˆ์ธก ์ˆ˜ํ–‰ (๋ชจ๋“  ๋ฏธ๋ž˜ ์‹œ์ ์— ๋™์ผํ•œ ๊ฐ’)
644
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
645
+
646
+ # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
647
+ std_error = np.std(last_values)
648
+ lower_bound = wma_value - 1.96 * std_error
649
+ upper_bound = wma_value + 1.96 * std_error
650
+
651
+ fc_df = pd.DataFrame({
652
+ 'ds': future_dates,
653
+ 'yhat': [wma_value] * len(future_dates),
654
+ 'yhat_lower': [lower_bound] * len(future_dates),
655
+ 'yhat_upper': [upper_bound] * len(future_dates)
656
+ })
657
+
658
+ # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
659
+ fc_df_monthly = pd.DataFrame({
660
+ 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
661
+ })
662
+
663
+ # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
664
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
665
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
666
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
667
+
668
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
669
+ fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
670
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
671
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
672
+
673
+ # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
674
+ fc_df_monthly['yearly'] = 0
675
+ fc_df_monthly['trend'] = fc_df_monthly['yhat']
676
+
677
+ return fc_df_monthly
678
+
679
+ except Exception as e:
680
+ st.error(f"๊ฐ€์ค‘ ์ด๋™ ํ‰๊ท  ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
681
+ return None
682
+
683
+ def fit_naive(df, horizon_end):
684
+ """๋‹จ์ˆœ Naive ๋ชจ๋ธ ๊ตฌํ˜„"""
685
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
686
+ monthly_df = prepare_monthly_data(df)
687
+
688
+ try:
689
+ # ๋งˆ์ง€๋ง‰ ๊ฐ’ ์‚ฌ์šฉ
690
+ last_value = monthly_df['price'].iloc[-1]
691
+
692
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
693
+ last_date = monthly_df.index[-1]
694
+ end_date = pd.Timestamp(horizon_end)
695
+ periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
696
+
697
+ # ์˜ˆ์ธก ์ˆ˜ํ–‰ (๋ชจ๋“  ๋ฏธ๋ž˜ ์‹œ์ ์— ๋งˆ์ง€๋ง‰ ๊ฐ’)
698
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
699
+
700
+ # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ • (๊ณผ๊ฑฐ 12๊ฐœ์›” ํ‘œ์ค€ํŽธ์ฐจ ์‚ฌ์šฉ)
701
+ history_std = monthly_df['price'].iloc[-12:].std() if len(monthly_df) >= 12 else monthly_df['price'].std()
702
+ lower_bound = last_value - 1.96 * history_std
703
+ upper_bound = last_value + 1.96 * history_std
704
+
705
+ fc_df = pd.DataFrame({
706
+ 'ds': future_dates,
707
+ 'yhat': [last_value] * len(future_dates),
708
+ 'yhat_lower': [lower_bound] * len(future_dates),
709
+ 'yhat_upper': [upper_bound] * len(future_dates)
710
+ })
711
+
712
+ # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
713
+ fc_df_monthly = pd.DataFrame({
714
+ 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
715
+ })
716
+
717
+ # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
718
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
719
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
720
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
721
+
722
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
723
+ fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
724
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
725
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
726
+
727
+ # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
728
+ fc_df_monthly['yearly'] = 0
729
+ fc_df_monthly['trend'] = fc_df_monthly['yhat']
730
+
731
+ return fc_df_monthly
732
+
733
+ except Exception as e:
734
+ st.error(f"Naive ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
735
+ return None
736
+
737
+ def fit_seasonal_naive(df, horizon_end):
738
+ """๊ณ„์ ˆ์„ฑ Naive ๋ชจ๋ธ ๊ตฌํ˜„"""
739
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
740
+ monthly_df = prepare_monthly_data(df)
741
+
742
+ try:
743
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
744
+ last_date = monthly_df.index[-1]
745
+ end_date = pd.Timestamp(horizon_end)
746
+ periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
747
+
748
+ # ์˜ˆ์ธก ์ˆ˜ํ–‰ (๊ฐ ์›”์— ๋Œ€ํ•ด ์ž‘๋…„ ๊ฐ™์€ ๋‹ฌ ๊ฐ€๊ฒฉ ์‚ฌ์šฉ)
749
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
750
+ future_values = []
751
+ lower_bounds = []
752
+ upper_bounds = []
753
+
754
+ for date in future_dates:
755
+ # ๊ฐ™์€ ์›”์˜ ๊ฐ’ ์ฐพ๊ธฐ
756
+ same_month_values = monthly_df[monthly_df.index.month == date.month]['price']
757
+
758
+ if len(same_month_values) > 0:
759
+ # ๊ฐ™์€ ์›” ๊ฐ€์žฅ ์ตœ๊ทผ ๊ฐ’ ์‚ฌ์šฉ
760
+ forecast_value = same_month_values.iloc[-1]
761
+
762
+ # ์‹ ๋ขฐ ๊ตฌ๊ฐ„
763
+ std_error = same_month_values.std() if len(same_month_values) > 1 else monthly_df['price'].std()
764
+ lower_bound = forecast_value - 1.96 * std_error
765
+ upper_bound = forecast_value + 1.96 * std_error
766
+ else:
767
+ # ๊ฐ™์€ ์›” ๋ฐ์ดํ„ฐ ์—†์œผ๋ฉด ์ „์ฒด ํ‰๊ท  ์‚ฌ์šฉ
768
+ forecast_value = monthly_df['price'].mean()
769
+ std_error = monthly_df['price'].std()
770
+ lower_bound = forecast_value - 1.96 * std_error
771
+ upper_bound = forecast_value + 1.96 * std_error
772
+
773
+ future_values.append(forecast_value)
774
+ lower_bounds.append(lower_bound)
775
+ upper_bounds.append(upper_bound)
776
+
777
+ fc_df = pd.DataFrame({
778
+ 'ds': future_dates,
779
+ 'yhat': future_values,
780
+ 'yhat_lower': lower_bounds,
781
+ 'yhat_upper': upper_bounds
782
+ })
783
+
784
+ # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
785
+ fc_df_monthly = pd.DataFrame({
786
+ 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
787
+ })
788
+
789
+ # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
790
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
791
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
792
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
793
+
794
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
795
+ fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
796
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
797
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
798
+
799
+ # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
800
+ fc_df_monthly['yearly'] = fc_df_monthly['yhat']
801
+ fc_df_monthly['trend'] = 0
802
+
803
+ return fc_df_monthly
804
+
805
+ except Exception as e:
806
+ st.error(f"Seasonal Naive ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
807
+ return None
808
+
809
+ def fit_fourier_lr(df, horizon_end):
810
+ """Fourier + ์„ ํ˜• ํšŒ๊ท€ ๋ชจ๋ธ ๊ตฌํ˜„"""
811
+ from sklearn.linear_model import LinearRegression
812
+
813
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
814
+ monthly_df = prepare_monthly_data(df)
815
+
816
+ try:
817
+ # ์‹œ๊ฐ„ ๋ณ€์ˆ˜ ์ƒ์„ฑ
818
+ y = monthly_df['price'].values
819
+ t = np.arange(len(y))
820
+
821
+ # Fourier ํŠน์„ฑ ์ƒ์„ฑ (์—ฐ๊ฐ„ ๊ณ„์ ˆ์„ฑ)
822
+ p = 12 # ์ฃผ๊ธฐ (1๋…„)
823
+ X = np.column_stack([
824
+ t, # ์„ ํ˜• ์ถ”์„ธ
825
+ np.sin(2 * np.pi * t / p),
826
+ np.cos(2 * np.pi * t / p),
827
+ np.sin(4 * np.pi * t / p),
828
+ np.cos(4 * np.pi * t / p)
829
+ ])
830
+
831
+ # ๋ชจ๋ธ ํ•™์Šต
832
+ model = LinearRegression()
833
+ model.fit(X, y)
834
+
835
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
836
+ last_date = monthly_df.index[-1]
837
+ end_date = pd.Timestamp(horizon_end)
838
+ periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
839
+
840
+ # ์˜ˆ์ธก ์ˆ˜ํ–‰
841
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
842
+
843
+ # ๋ฏธ๋ž˜ ์‹œ์  ํŠน์„ฑ ์ƒ์„ฑ
844
+ t_future = np.arange(len(y), len(y) + periods)
845
+ X_future = np.column_stack([
846
+ t_future,
847
+ np.sin(2 * np.pi * t_future / p),
848
+ np.cos(2 * np.pi * t_future / p),
849
+ np.sin(4 * np.pi * t_future / p),
850
+ np.cos(4 * np.pi * t_future / p)
851
+ ])
852
+
853
+ # ์˜ˆ์ธก
854
+ forecast = model.predict(X_future)
855
+
856
+ # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
857
+ y_pred = model.predict(X)
858
+ mse = np.mean((y - y_pred) ** 2)
859
+ std_error = np.sqrt(mse)
860
+
861
+ lower_bound = forecast - 1.96 * std_error
862
+ upper_bound = forecast + 1.96 * std_error
863
+
864
+ fc_df = pd.DataFrame({
865
+ 'ds': future_dates,
866
+ 'yhat': forecast,
867
+ 'yhat_lower': lower_bound,
868
+ 'yhat_upper': upper_bound
869
+ })
870
+
871
+ # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
872
+ fc_df_monthly = pd.DataFrame({
873
+ 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
874
+ })
875
+
876
+ # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
877
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
878
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
879
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
880
+
881
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
882
+ fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
883
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
884
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
885
+
886
+ # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
887
+ fc_df_monthly['trend'] = model.coef_[0] * np.arange(len(fc_df_monthly)) + model.intercept_
888
+
889
+ # ๊ณ„์ ˆ์„ฑ ๊ณ„์‚ฐ
890
+ season_features = np.column_stack([
891
+ np.sin(2 * np.pi * np.arange(len(fc_df_monthly)) / p),
892
+ np.cos(2 * np.pi * np.arange(len(fc_df_monthly)) / p),
893
+ np.sin(4 * np.pi * np.arange(len(fc_df_monthly)) / p),
894
+ np.cos(4 * np.pi * np.arange(len(fc_df_monthly)) / p)
895
+ ])
896
+
897
+ seasonal_effect = np.dot(season_features, model.coef_[1:5])
898
+ fc_df_monthly['yearly'] = seasonal_effect
899
+
900
+ return fc_df_monthly
901
+
902
+ except Exception as e:
903
+ st.error(f"Fourier + LR ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
904
+ return None
905
+
906
+ def fit_linear_trend(df, horizon_end):
907
+ """์„ ํ˜• ์ถ”์„ธ ๋ชจ๋ธ ๊ตฌํ˜„"""
908
+ from sklearn.linear_model import LinearRegression
909
+
910
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
911
+ monthly_df = prepare_monthly_data(df)
912
+
913
+ try:
914
+ # ์‹œ๊ฐ„ ๋ณ€์ˆ˜ ์ƒ์„ฑ
915
+ y = monthly_df['price'].values
916
+ t = np.arange(len(y)).reshape(-1, 1)
917
+
918
+ # ๋ชจ๋ธ ํ•™์Šต
919
+ model = LinearRegression()
920
+ model.fit(t, y)
921
+
922
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
923
+ last_date = monthly_df.index[-1]
924
+ end_date = pd.Timestamp(horizon_end)
925
+ periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
926
+
927
+ # ์˜ˆ์ธก ์ˆ˜ํ–‰
928
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
929
+ t_future = np.arange(len(y), len(y) + periods).reshape(-1, 1)
930
+ forecast = model.predict(t_future)
931
+
932
+ # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
933
+ y_pred = model.predict(t)
934
+ mse = np.mean((y - y_pred) ** 2)
935
+ std_error = np.sqrt(mse)
936
+
937
+ lower_bound = forecast - 1.96 * std_error
938
+ upper_bound = forecast + 1.96 * std_error
939
+
940
+ fc_df = pd.DataFrame({
941
+ 'ds': future_dates,
942
+ 'yhat': forecast,
943
+ 'yhat_lower': lower_bound,
944
+ 'yhat_upper': upper_bound
945
+ })
946
+
947
+ # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
948
+ fc_df_monthly = pd.DataFrame({
949
+ 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
950
+ })
951
+
952
+ # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
953
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
954
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
955
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
956
+
957
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
958
+ fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
959
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
960
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
961
+
962
+ # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
963
+ fc_df_monthly['yearly'] = 0
964
+ fc_df_monthly['trend'] = fc_df_monthly['yhat']
965
+
966
+ return fc_df_monthly
967
+
968
+ except Exception as e:
969
+ st.error(f"Linear Trend ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
970
+ return None
971
 
972
+ def fit_simple_exp_smoothing(df, horizon_end):
973
+ """๋‹จ์ˆœ ์ง€์ˆ˜ ํ‰ํ™œ ๋ชจ๋ธ ๊ตฌํ˜„"""
974
+ # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
975
+ monthly_df = prepare_monthly_data(df)
976
+
977
+ try:
978
+ # ๋ชจ๋ธ ํ•™์Šต
979
+ model = SimpleExpSmoothing(monthly_df['price'])
980
+ results = model.fit(optimized=True)
981
+
982
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
983
+ last_date = monthly_df.index[-1]
984
+ end_date = pd.Timestamp(horizon_end)
985
+ periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
986
+
987
+ # ์˜ˆ์ธก ์ˆ˜ํ–‰
988
+ forecast = results.forecast(periods)
989
+
990
+ # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
991
+ std_error = np.std(results.resid)
992
+ lower_bound = forecast - 1.96 * std_error
993
+ upper_bound = forecast + 1.96 * std_error
994
+
995
+ # Prophet ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
996
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
997
+
998
+ fc_df = pd.DataFrame({
999
+ 'ds': future_dates,
1000
+ 'yhat': forecast.values,
1001
+ 'yhat_lower': lower_bound.values,
1002
+ 'yhat_upper': upper_bound.values
1003
+ })
1004
+
1005
+ # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
1006
+ fc_df_monthly = pd.DataFrame({
1007
+ 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
1008
+ })
1009
+
1010
+ # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
1011
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
1012
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
1013
+ fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
1014
+
1015
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
1016
+ fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
1017
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
1018
+ fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
1019
+
1020
+ # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
1021
+ fc_df_monthly['yearly'] = 0
1022
+ fc_df_monthly['trend'] = fc_df_monthly['yhat']
1023
+
1024
+ return fc_df_monthly
1025
+
1026
+ except Exception as e:
1027
+ st.error(f"Simple Exponential Smoothing ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
1028
+ return None
1029
+
1030
+ @st.cache_data(show_spinner=False, ttl=3600)
1031
+ def fit_optimal_model(df, item_name, horizon_end, model_type="primary"):
1032
+ """ํ’ˆ๋ชฉ๋ณ„ ์ตœ์  ๋ชจ๋ธ ์ ์šฉ"""
1033
+ # ๋ฐ์ดํ„ฐ ์ค€๋น„ ๋ฐ ์ •๋ฆฌ
1034
+ df = df.copy()
1035
+ df = df.dropna(subset=["date", "price"])
1036
+
1037
+ # ํ’ˆ๋ชฉ๋ณ„ ์ตœ์  ๋ชจ๋ธ ์„ ํƒ
1038
+ model_info = get_best_model_for_item(item_name)
1039
+
1040
+ if model_type == "primary":
1041
+ model_name = model_info["model1"]
1042
+ accuracy = model_info["accuracy1"]
1043
+ else: # backup
1044
+ model_name = model_info["model2"]
1045
+ accuracy = model_info["accuracy2"]
1046
+
1047
+ st.info(f"{item_name}์— ์ตœ์ ํ™”๋œ {model_name} ๋ชจ๋ธ ์ ์šฉ (์ •ํ™•๋„: {accuracy}%)")
1048
+
1049
+ # ํŠน์ˆ˜ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ํ’ˆ๋ชฉ ํ™•์ธ
1050
+ needs_monitoring = "special" in model_info and model_info["special"] == "accuracy_drop"
1051
+ if needs_monitoring:
1052
+ st.warning(f"โš ๏ธ {item_name}๋Š” ํŠน์ • ์›”์— ์ •ํ™•๋„๊ฐ€ ๊ธ‰๋ฝํ•  ์ˆ˜ ์žˆ๋Š” ํ’ˆ๋ชฉ์ž…๋‹ˆ๋‹ค. ์˜ˆ์ธก ๊ฒฐ๊ณผ๋ฅผ ์ฃผ์˜ ๊นŠ๊ฒŒ ์‚ดํŽด๋ณด์„ธ์š”.")
1053
+
1054
+ # ๋ชจ๋ธ ์„ ํƒ ๋ฐ ํ•™์Šต
1055
+ if "SARIMA(1,0,1)(1,0,1,12)" in model_name:
1056
+ return fit_sarima(df, order=(1,0,1), seasonal_order=(1,0,1,12), horizon_end=horizon_end)
1057
+ elif "SARIMA(1,1,1)(1,1,1,12)" in model_name:
1058
+ return fit_sarima(df, order=(1,1,1), seasonal_order=(1,1,1,12), horizon_end=horizon_end)
1059
+ elif "SARIMA(0,1,1)(0,1,1,12)" in model_name:
1060
+ return fit_sarima(df, order=(0,1,1), seasonal_order=(0,1,1,12), horizon_end=horizon_end)
1061
+ elif "ETS(Multiplicative)" in model_name:
1062
+ return fit_ets(df, seasonal_type="multiplicative", horizon_end=horizon_end)
1063
+ elif "ETS(Additive)" in model_name:
1064
+ return fit_ets(df, seasonal_type="additive", horizon_end=horizon_end)
1065
+ elif "Holt-Winters" in model_name:
1066
+ return fit_holt_winters(df, horizon_end=horizon_end)
1067
+ elif "Holt" in model_name:
1068
+ return fit_holt(df, horizon_end=horizon_end)
1069
+ elif "MovingAverage-6 m" in model_name:
1070
+ return fit_moving_average(df, window=6, horizon_end=horizon_end)
1071
+ elif "WeightedMA-6 m" in model_name:
1072
+ return fit_weighted_ma(df, window=6, horizon_end=horizon_end)
1073
+ elif "Naive" in model_name and "Seasonal" not in model_name:
1074
+ return fit_naive(df, horizon_end=horizon_end)
1075
+ elif "SeasonalNaive" in model_name:
1076
+ return fit_seasonal_naive(df, horizon_end=horizon_end)
1077
+ elif "Fourier + LR" in model_name:
1078
+ return fit_fourier_lr(df, horizon_end=horizon_end)
1079
+ elif "LinearTrend" in model_name:
1080
+ return fit_linear_trend(df, horizon_end=horizon_end)
1081
+ elif "SimpleExpSmoothing" in model_name:
1082
+ return fit_simple_exp_smoothing(df, horizon_end=horizon_end)
1083
+ else:
1084
+ st.warning(f"์•Œ ์ˆ˜ ์—†๋Š” ๋ชจ๋ธ: {model_name}. ๊ธฐ๋ณธ ๋ชจ๋ธ(SARIMA)์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
1085
+ return fit_sarima(df, order=(1,0,1), seasonal_order=(1,0,1,12), horizon_end=horizon_end)
1086
+
1087
+ def fit_ensemble_model(df, item_name, horizon_end):
1088
+ """1์œ„์™€ 2์œ„ ๋ชจ๋ธ์˜ ์•™์ƒ๋ธ” ์ˆ˜ํ–‰"""
1089
+ # 1์œ„ ๋ชจ๋ธ ์˜ˆ์ธก
1090
+ fc1 = fit_optimal_model(df, item_name, horizon_end, model_type="primary")
1091
+
1092
+ # 2์œ„ ๋ชจ๋ธ ์˜ˆ์ธก
1093
+ fc2 = fit_optimal_model(df, item_name, horizon_end, model_type="backup")
1094
+
1095
+ # ๋‘ ๋ชจ๋ธ ๋ชจ๋‘ ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ๋งŒ ์•™์ƒ๋ธ”
1096
+ if fc1 is not None and fc2 is not None:
1097
+ # ์•™์ƒ๋ธ” ๊ฐ€์ค‘์น˜ ๊ณ„์‚ฐ (์ •ํ™•๋„ ๊ธฐ๋ฐ˜)
1098
+ model_info = get_best_model_for_item(item_name)
1099
+ acc1 = model_info["accuracy1"]
1100
+ acc2 = model_info["accuracy2"]
1101
+
1102
+ # ์ •ํ™•๋„ ์ฐจ์ด๊ฐ€ 0.2%p ์ด๋‚ด์ธ ๊ฒฝ์šฐ ์•™์ƒ๋ธ” ์ˆ˜ํ–‰
1103
+ accuracy_diff = abs(acc1 - acc2)
1104
+
1105
+ if accuracy_diff <= 0.2:
1106
+ st.success(f"๋‘ ๋ชจ๋ธ์˜ ์ •ํ™•๋„ ์ฐจ์ด๊ฐ€ {accuracy_diff:.2f}%p๋กœ ์ž‘์•„ ์•™์ƒ๋ธ”์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.")
1107
+
1108
+ # ์ •ํ™•๋„ ๊ธฐ๋ฐ˜ ๊ฐ€์ค‘์น˜ ๊ณ„์‚ฐ
1109
+ total_acc = acc1 + acc2
1110
+ w1 = acc1 / total_acc
1111
+ w2 = acc2 / total_acc
1112
+
1113
+ # ์•™์ƒ๋ธ” ๊ฒฐ๊ณผ ์ƒ์„ฑ
1114
+ fc_ensemble = fc1.copy()
1115
+ fc_ensemble['yhat'] = w1 * fc1['yhat'] + w2 * fc2['yhat']
1116
+ fc_ensemble['yhat_lower'] = w1 * fc1['yhat_lower'] + w2 * fc2['yhat_lower']
1117
+ fc_ensemble['yhat_upper'] = w1 * fc1['yhat_upper'] + w2 * fc2['yhat_upper']
1118
+
1119
+ return fc_ensemble
1120
+ else:
1121
+ st.info(f"์ •ํ™•๋„ ์ฐจ์ด๊ฐ€ {accuracy_diff:.2f}%p๋กœ ์ปค์„œ 1์œ„ ๋ชจ๋ธ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
1122
+ return fc1
1123
+
1124
+ # ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•œ ๊ฒฝ์šฐ ์„ฑ๊ณตํ•œ ๋ชจ๋ธ ๋ฐ˜ํ™˜
1125
+ return fc1 if fc1 is not None else fc2
1126
 
1127
  # -------------------------------------------------
1128
+ # MAIN APP ---------------------------------------
1129
  # -------------------------------------------------
1130
+ # ๋ฐ์ดํ„ฐ ๋กœ๋“œ
1131
  raw_df = load_data()
1132
 
1133
  if len(raw_df) == 0:
 
1139
  current_date = date.today()
1140
  st.sidebar.caption(f"์˜ค๋Š˜: {current_date}")
1141
 
1142
+ # ์„ ํƒ๋œ ํ’ˆ๋ชฉ์˜ ์ตœ์  ๋ชจ๋ธ ์ •๋ณด ํ‘œ์‹œ
1143
+ model_info = get_best_model_for_item(selected_item)
1144
+ st.sidebar.subheader("ํ’ˆ๋ชฉ๋ณ„ ์ตœ์  ๋ชจ๋ธ")
1145
+ st.sidebar.markdown(f"**1์œ„ ๋ชจ๋ธ:** {model_info['model1']} (์ •ํ™•๋„: {model_info['accuracy1']}%)")
1146
+ st.sidebar.markdown(f"**2์œ„ ๋ชจ๋ธ:** {model_info['model2']} (์ •ํ™•๋„: {model_info['accuracy2']}%)")
1147
+
1148
+ # ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง
1149
  item_df = raw_df.query("item == @selected_item").copy()
1150
  if item_df.empty:
1151
  st.error("์„ ํƒํ•œ ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ ์—†์Œ")
 
1180
 
1181
  if len(macro_df) < 2:
1182
  st.warning(f"{selected_item}์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ „์ฒด ๊ธฐ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.")
1183
+ fig = go.Figure()
1184
+ fig.add_trace(go.Scatter(x=item_df["date"], y=item_df["price"], mode="lines", name="์‹ค์ œ ๊ฐ€๊ฒฉ"))
1185
+ fig.update_layout(title=f"{selected_item} ๊ณผ๊ฑฐ ๊ฐ€๊ฒฉ")
1186
  st.plotly_chart(fig, use_container_width=True)
1187
  else:
1188
  try:
1189
+ # ๋ฐ์ดํ„ฐ ์ถฉ๋ถ„ํ•œ ๊ฒฝ์šฐ ํ’ˆ๋ชฉ๋ณ„ ์ตœ์  ๋ชจ๋ธ ์‚ฌ์šฉ
1190
+ use_ensemble = st.checkbox("์•™์ƒ๋ธ” ๋ชจ๋ธ ์‚ฌ์šฉ (1์œ„ + 2์œ„ ๋ชจ๋ธ ๊ฒฐํ•ฉ)", value=False)
1191
+
1192
  with st.spinner("์žฅ๊ธฐ ์˜ˆ์ธก ๋ชจ๋ธ ์ƒ์„ฑ ์ค‘..."):
1193
+ if use_ensemble:
1194
+ fc_macro = fit_ensemble_model(macro_df, selected_item, MACRO_END)
1195
+ else:
1196
+ fc_macro = fit_optimal_model(macro_df, selected_item, MACRO_END)
1197
 
1198
+ if fc_macro is not None:
1199
  # ์‹ค์ œ ๋ฐ์ดํ„ฐ์™€ ์˜ˆ์ธก ๋ฐ์ดํ„ฐ ๊ตฌ๋ถ„
1200
  cutoff_date = pd.Timestamp("2025-01-01")
1201
 
 
1213
  line=dict(color="blue", width=2)
1214
  ))
1215
 
1216
+ # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ์ž๋ฅด๊ธฐ
1217
  forecast_data = fc_macro[fc_macro["ds"] >= cutoff_date].copy()
1218
+
1219
+ # 2025-2030 ์˜ˆ์ธก ๋ฐ์ดํ„ฐ
1220
  if not forecast_data.empty:
1221
  fig.add_trace(go.Scatter(
1222
  x=forecast_data["ds"],
 
1244
  name="95% ์‹ ๋ขฐ ๊ตฌ๊ฐ„"
1245
  ))
1246
 
1247
+ # ์Œ์ˆ˜ ์˜ˆ์ธก๊ฐ’ ์ œ๊ฑฐ
1248
+ fig.update_yaxes(range=[0, None])
1249
+
1250
  # ๋ ˆ์ด์•„์›ƒ ์„ค์ •
1251
  fig.update_layout(
1252
  title=f"{selected_item} ์žฅ๊ธฐ ๊ฐ€๊ฒฉ ์˜ˆ์ธก (1996-2030)",
 
1282
  st.error(f"์˜ˆ์ธก๊ฐ€ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {str(e)}")
1283
  else:
1284
  st.warning("์˜ˆ์ธก ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
1285
+ fig = go.Figure()
1286
+ fig.add_trace(go.Scatter(x=macro_df["date"], y=macro_df["price"], mode="lines", name="์‹ค์ œ ๊ฐ€๊ฒฉ"))
1287
+ fig.update_layout(title=f"{selected_item} ๊ณผ๊ฑฐ ๊ฐ€๊ฒฉ")
1288
  st.plotly_chart(fig, use_container_width=True)
1289
  except Exception as e:
1290
  st.error(f"์žฅ๊ธฐ ์˜ˆ์ธก ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
1291
+ import traceback
1292
+ st.code(traceback.format_exc())
1293
+ fig = go.Figure()
1294
+ fig.add_trace(go.Scatter(x=macro_df["date"], y=macro_df["price"], mode="lines", name="์‹ค์ œ ๊ฐ€๊ฒฉ"))
1295
+ fig.update_layout(title=f"{selected_item} ๊ณผ๊ฑฐ ๊ฐ€๊ฒฉ")
1296
  st.plotly_chart(fig, use_container_width=True)
1297
 
1298
  # -------------------------------------------------
 
1314
 
1315
  if len(micro_df) < 2:
1316
  st.warning(f"์ตœ๊ทผ ๋ฐ์ดํ„ฐ๊ฐ€ ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
1317
+ fig = go.Figure()
1318
+ fig.add_trace(go.Scatter(x=item_df["date"], y=item_df["price"], mode="lines", name="์‹ค์ œ ๊ฐ€๊ฒฉ"))
1319
+ fig.update_layout(title=f"{selected_item} ์ตœ๊ทผ ๊ฐ€๊ฒฉ")
1320
  st.plotly_chart(fig, use_container_width=True)
1321
  else:
1322
  try:
1323
  with st.spinner("๋‹จ๊ธฐ ์˜ˆ์ธก ๋ชจ๋ธ ์ƒ์„ฑ ์ค‘..."):
1324
+ if use_ensemble:
1325
+ fc_micro = fit_ensemble_model(micro_df, selected_item, MICRO_END)
1326
+ else:
1327
+ fc_micro = fit_optimal_model(micro_df, selected_item, MICRO_END)
1328
 
1329
+ if fc_micro is not None:
1330
  # 2024-01-01๋ถ€ํ„ฐ 2026-12-31๊นŒ์ง€ ํ•„ํ„ฐ๋ง
1331
  start_date = pd.Timestamp("2024-01-01")
1332
  end_date = pd.Timestamp("2026-12-31")
 
1400
  name="95% ์‹ ๋ขฐ ๊ตฌ๊ฐ„"
1401
  ))
1402
 
1403
+ # ์Œ์ˆ˜ ์˜ˆ์ธก๊ฐ’ ์ œ๊ฑฐ
1404
+ fig.update_yaxes(range=[0, None])
1405
+
1406
  # ๋ ˆ์ด์•„์›ƒ ์„ค์ •
1407
  fig.update_layout(
1408
  title=f"{selected_item} ์›”๋ณ„ ๋‹จ๊ธฐ ์˜ˆ์ธก (2024-2026)",
 
1463
  # -------------------------------------------------
1464
  # SEASONALITY & PATTERN ---------------------------
1465
  # -------------------------------------------------
1466
+ if 'fc_micro' in locals() and fc_micro is not None:
1467
+ with st.expander("๐Ÿ“† ์‹œ์ฆˆ๋„๋ฆฌํ‹ฐ & ํŒจํ„ด ์„ค๋ช…"):
1468
  try:
1469
+ # ์›”๋ณ„ ๊ณ„์ ˆ์„ฑ ๋ถ„์„
1470
+ if "yearly" in fc_micro.columns and fc_micro["yearly"].sum() != 0:
1471
+ month_season = fc_micro.copy()
1472
+ month_season["month"] = month_season["ds"].dt.month
1473
+ month_seasonality = month_season.groupby("month")["yearly"].mean()
1474
+
1475
+ # ์›” ์ด๋ฆ„ ์„ค์ •
1476
+ month_names = ["1์›”", "2์›”", "3์›”", "4์›”", "5์›”", "6์›”", "7์›”", "8์›”", "9์›”", "10์›”", "11์›”", "12์›”"]
1477
+
1478
+ # ๊ณ„์ ˆ์„ฑ ์ฐจํŠธ ๊ทธ๋ฆฌ๊ธฐ
1479
+ fig = go.Figure()
1480
+ fig.add_trace(go.Bar(
1481
+ x=month_names,
1482
+ y=month_seasonality.values,
1483
+ marker_color=['blue' if x >= 0 else 'red' for x in month_seasonality.values]
1484
+ ))
1485
+
1486
+ fig.update_layout(
1487
+ title=f"{selected_item} ์›”๋ณ„ ๊ณ„์ ˆ์„ฑ ํŒจํ„ด",
1488
+ xaxis_title="์›”",
1489
+ yaxis_title="์ƒ๋Œ€์  ๊ฐ€๊ฒฉ ๋ณ€๋™",
1490
+ )
1491
+
1492
+ st.plotly_chart(fig, use_container_width=True)
1493
+
1494
+ # ํ”ผํฌ์™€ ์ €์  ๊ณ„์‚ฐ
1495
+ peak_month = month_seasonality.idxmax()
1496
+ low_month = month_seasonality.idxmin()
1497
+ seasonality_range = month_seasonality.max() - month_seasonality.min()
1498
+
1499
+ st.markdown(
1500
+ f"**์—ฐ๊ฐ„ ํ”ผํฌ ์›”:** {month_names[peak_month-1]} \n"
1501
+ f"**์—ฐ๊ฐ„ ์ €์  ์›”:** {month_names[low_month-1]} \n"
1502
+ f"**์—ฐ๊ฐ„ ๋ณ€๋™ํญ:** {seasonality_range:.1f}")
1503
+
1504
+ # ๊ณ„์ ˆ์„ฑ์ด ๋†’์€ ํ’ˆ๋ชฉ์ธ์ง€ ์„ค๋ช…
1505
+ if abs(seasonality_range) > 30:
1506
+ st.info(f"{selected_item}์€(๋Š”) ๊ณ„์ ˆ์„ฑ์ด ๋งค์šฐ ๊ฐ•ํ•œ ํ’ˆ๋ชฉ์ž…๋‹ˆ๋‹ค. ํŠน์ • ๋‹ฌ์— ๊ฐ€๊ฒฉ์ด ํฌ๊ฒŒ ๋ณ€๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
1507
+ elif abs(seasonality_range) > 10:
1508
+ st.info(f"{selected_item}์€(๋Š”) ๊ณ„์ ˆ์„ฑ์ด ์ค‘๊ฐ„ ์ •๋„์ธ ํ’ˆ๋ชฉ์ž…๋‹ˆ๋‹ค.")
1509
+ else:
1510
+ st.info(f"{selected_item}์€(๋Š”) ๊ณ„์ ˆ์„ฑ์ด ์•ฝํ•œ ํ’ˆ๋ชฉ์ž…๋‹ˆ๋‹ค. ์—ฐ์ค‘ ๊ฐ€๊ฒฉ์ด ๋น„๊ต์  ์•ˆ์ •์ ์ž…๋‹ˆ๋‹ค.")
1511
  except Exception as e:
1512
+ st.error(f"๊ณ„์ ˆ์„ฑ ๋ถ„์„ ์˜ค๋ฅ˜: {str(e)}")
1513
+ st.info("์ด ํ’ˆ๋ชฉ์— ๋Œ€ํ•œ ๊ณ„์ ˆ์„ฑ ํŒจํ„ด์„ ๋ถ„์„ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
 
1514
 
1515
  # -------------------------------------------------
1516
  # FOOTER ------------------------------------------