openfree commited on
Commit
d8245c1
ยท
verified ยท
1 Parent(s): eb7c361

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +20 -1673
app.py CHANGED
@@ -1,1688 +1,35 @@
 
 
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 ------------------------------------------
28
- # -------------------------------------------------
29
- CSV_PATH = Path("2025-domae.csv")
30
- MACRO_START, MACRO_END = "1996-01-01", "2030-12-31"
31
- MICRO_START, MICRO_END = "2024-01-01", "2026-12-31"
32
-
33
-
34
- # ํ•œ๊ธ€ ํฐํŠธ ์„ค์ •
35
- font_list = [f.name for f in fm.fontManager.ttflist if 'gothic' in f.name.lower() or
36
- 'gulim' in f.name.lower() or 'malgun' in f.name.lower() or
37
- 'nanum' in f.name.lower() or 'batang' in f.name.lower()]
38
-
39
- if font_list:
40
- font_name = font_list[0]
41
- plt.rcParams['font.family'] = font_name
42
- mpl.rcParams['axes.unicode_minus'] = False
43
- else:
44
- plt.rcParams['font.family'] = 'DejaVu Sans'
45
-
46
- st.set_page_config(page_title="ํ’ˆ๋ชฉ๋ณ„ ๊ฐ€๊ฒฉ ์˜ˆ์ธก", page_icon="๐Ÿ“ˆ", layout="wide")
47
-
48
- # -------------------------------------------------
49
- # ํ’ˆ๋ชฉ๋ณ„ ์ตœ์  ๋ชจ๋ธ ๋งคํ•‘ ---------------------------
50
- # -------------------------------------------------
51
- item_models = {
52
- "๊ฐˆ์น˜": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.82, "model2": "Holt-Winters", "accuracy2": 99.80},
53
- "๊ฐ์ž": {"model1": "ETS(Multiplicative)", "accuracy1": 99.58, "model2": "SARIMA(1,0,1)(1,0,1,12)", "accuracy2": 98.70},
54
- "๊ฑด๊ณ ์ถ”": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.96, "model2": "Holt", "accuracy2": 99.79},
55
- "๊ฑด๋‹ค์‹œ๋งˆ": {"model1": "Naive", "accuracy1": 99.59, "model2": "SeasonalNaive", "accuracy2": 99.34},
56
- "๊ณ ๊ตฌ๋งˆ": {"model1": "SARIMA(1,1,1)(1,1,1,12)", "accuracy1": 99.89, "model2": "ETS(Multiplicative)", "accuracy2": 98.91},
57
- "๊ณ ๋“ฑ์–ด": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.48, "model2": "ETS(Additive)", "accuracy2": 99.42},
58
- "๊น€": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.99, "model2": "SARIMA(0,1,1)(0,1,1,12)", "accuracy2": 99.93},
59
- "๊น๋งˆ๋Š˜(๊ตญ์‚ฐ)": {"model1": "SeasonalNaive", "accuracy1": 99.79, "model2": "MovingAverage-6 m", "accuracy2": 98.65},
60
- "๊นป์žŽ": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.68, "model2": "Holt", "accuracy2": 99.54},
61
- "๋…น๋‘": {"model1": "WeightedMA-6 m", "accuracy1": 99.53, "model2": "Fourier + LR", "accuracy2": 99.53},
62
- "๋Аํƒ€๋ฆฌ๋ฒ„์„ฏ": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.84, "model2": "LinearTrend", "accuracy2": 99.80},
63
- "๋‹น๊ทผ": {"model1": "Holt", "accuracy1": 99.25, "model2": "ETS(Multiplicative)", "accuracy2": 97.27},
64
- "๋“ค๊นจ": {"model1": "Holt", "accuracy1": 99.57, "model2": "Holt-Winters", "accuracy2": 99.17},
65
- "๋•…์ฝฉ": {"model1": "SARIMA(1,1,1)(1,1,1,12)", "accuracy1": 99.74, "model2": "ETS(Additive)", "accuracy2": 99.37},
66
- "๋ ˆ๋ชฌ": {"model1": "WeightedMA-6 m", "accuracy1": 99.99, "model2": "LinearTrend", "accuracy2": 98.99},
67
- "๋ง๊ณ ": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.38, "model2": "Holt-Winters", "accuracy2": 99.02},
68
- "๋ฉ”๋ฐ€": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.48, "model2": "SARIMA(0,1,1)(0,1,1,12)", "accuracy2": 98.99},
69
- "๋ฉœ๋ก ": {"model1": "Naive", "accuracy1": 99.07, "model2": "ETS(Multiplicative)", "accuracy2": 99.01},
70
- "๋ช…ํƒœ": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 100.00, "model2": "MovingAverage-6 m", "accuracy2": 99.93},
71
- "๋ฌด": {"model1": "SARIMA(1,1,1)(1,1,1,12)", "accuracy1": 99.54, "model2": "SeasonalNaive", "accuracy2": 88.29, "special": "accuracy_drop"},
72
- "๋ฌผ์˜ค์ง•์–ด": {"model1": "Holt-Winters", "accuracy1": 99.91, "model2": "ETS(Multiplicative)", "accuracy2": 99.36},
73
- "๋ฏธ๋‚˜๋ฆฌ": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 98.71, "model2": "LinearTrend", "accuracy2": 98.54},
74
- "๋ฐ”๋‚˜๋‚˜": {"model1": "MovingAverage-6 m", "accuracy1": 99.81, "model2": "ETS(Multiplicative)", "accuracy2": 98.86},
75
- "๋ฐฉ์šธํ† ๋งˆํ† ": {"model1": "ETS(Multiplicative)", "accuracy1": 99.62, "model2": "Holt", "accuracy2": 98.28},
76
- "๋ฐฐ": {"model1": "ETS(Additive)", "accuracy1": 99.34, "model2": "LinearTrend", "accuracy2": 98.57},
77
- "๋ฐฐ์ถ”": {"model1": "Holt", "accuracy1": 99.98, "model2": "MovingAverage-6 m", "accuracy2": 99.71},
78
- "๋ถ์–ด": {"model1": "Fourier + LR", "accuracy1": 99.96, "model2": "MovingAverage-6 m", "accuracy2": 99.94},
79
- "๋ถ‰์€๊ณ ์ถ”": {"model1": "SARIMA(1,1,1)(1,1,1,12)", "accuracy1": 99.75, "model2": "LinearTrend", "accuracy2": 97.61},
80
- "๋ธŒ๋กœ์ฝœ๋ฆฌ": {"model1": "Holt", "accuracy1": 99.54, "model2": "Naive", "accuracy2": 99.93},
81
- "์‚ฌ๊ณผ": {"model1": "Holt-Winters", "accuracy1": 99.89, "model2": "ETS(Multiplicative)", "accuracy2": 98.91},
82
- "์ƒ์ถ”": {"model1": "ETS(Additive)", "accuracy1": 99.11, "model2": "Holt-Winters", "accuracy2": 97.61},
83
- "์ƒˆ์†ก์ด๋ฒ„์„ฏ": {"model1": "SimpleExpSmoothing", "accuracy1": 99.95, "model2": "Holt-Winters", "accuracy2": 99.40},
84
- "์ƒˆ์šฐ": {"model1": "ETS(Additive)", "accuracy1": 99.87, "model2": "Naive", "accuracy2": 99.96},
85
- "์ƒ๊ฐ•": {"model1": "Naive", "accuracy1": 99.27, "model2": "ETS(Additive)", "accuracy2": 98.53},
86
- "์ˆ˜๋ฐ•": {"model1": "Naive", "accuracy1": 99.91, "model2": "SARIMA(1,1,1)(1,1,1,12)", "accuracy2": 99.45},
87
- "์‹œ๊ธˆ์น˜": {"model1": "Holt-Winters", "accuracy1": 99.70, "model2": "SeasonalNaive", "accuracy2": 98.73},
88
- "์Œ€": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.99, "model2": "Holt-Winters", "accuracy2": 99.88},
89
- "์•Œ๋ฐฐ๊ธฐ๋ฐฐ์ถ”": {"model1": "WeightedMA-6 m", "accuracy1": 98.19, "model2": "SeasonalNaive", "accuracy2": 95.73},
90
- "์–‘๋ฐฐ์ถ”": {"model1": "Holt-Winters", "accuracy1": 99.05, "model2": "WeightedMA-6 m", "accuracy2": 97.85},
91
- "์–‘ํŒŒ": {"model1": "ETS(Additive)", "accuracy1": 99.93, "model2": "WeightedMA-6 m", "accuracy2": 99.51},
92
- "์–ผ๊ฐˆ์ด๋ฐฐ์ถ”": {"model1": "SARIMA(1,1,1)(1,1,1,12)", "accuracy1": 99.77, "model2": "SeasonalNaive", "accuracy2": 98.55},
93
- "์—ด๋ฌด": {"model1": "SeasonalNaive", "accuracy1": 99.96, "model2": "Holt", "accuracy2": 99.50},
94
- "์˜ค์ด": {"model1": "SeasonalNaive", "accuracy1": 99.82, "model2": "ETS(Additive)", "accuracy2": 98.48},
95
- "์ „๋ณต": {"model1": "Holt", "accuracy1": 99.90, "model2": "Fourier + LR", "accuracy2": 99.90},
96
- "์ฐธ๊นจ": {"model1": "WeightedMA-6 m", "accuracy1": 100.00, "model2": "LinearTrend", "accuracy2": 86.44, "special": "accuracy_drop"},
97
- "์ฐน์Œ€": {"model1": "SARIMA(1,0,1)(1,0,1,12)", "accuracy1": 99.71, "model2": "Naive", "accuracy2": 98.64, "special": "accuracy_drop"},
98
- "์ฝฉ": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.98, "model2": "ETS(Additive)", "accuracy2": 99.68},
99
- "ํ† ๋งˆํ† ": {"model1": "SeasonalNaive", "accuracy1": 97.31, "model2": "MovingAverage-6 m", "accuracy2": 97.57},
100
- "ํŒŒ": {"model1": "MovingAverage-6 m", "accuracy1": 99.92, "model2": "Holt-Winters", "accuracy2": 97.77},
101
- "ํŒŒ์ธ์• ํ”Œ": {"model1": "Naive", "accuracy1": 99.51, "model2": "SARIMA(1,0,1)(1,0,1,12)", "accuracy2": 96.39},
102
- "ํŒŒํ”„๋ฆฌ์นด": {"model1": "SARIMA(0,1,1)(0,1,1,12)", "accuracy1": 99.04, "model2": "WeightedMA-6 m", "accuracy2": 99.36},
103
- "ํŒฅ": {"model1": "ETS(Additive)", "accuracy1": 99.87, "model2": "Holt-Winters", "accuracy2": 75.08, "special": "accuracy_drop"},
104
- "ํŒฝ์ด๋ฒ„์„ฏ": {"model1": "SeasonalNaive", "accuracy1": 99.84, "model2": "Fourier + LR", "accuracy2": 98.49},
105
- "ํ’‹๊ณ ์ถ”": {"model1": "Holt-Winters", "accuracy1": 98.95, "model2": "ETS(Multiplicative)", "accuracy2": 98.73},
106
- "ํ”ผ๋ง": {"model1": "Fourier + LR", "accuracy1": 99.64, "model2": "WeightedMA-6 m", "accuracy2": 98.93},
107
- "ํ˜ธ๋ฐ•": {"model1": "ETS(Multiplicative)", "accuracy1": 99.98, "model2": "SeasonalNaive", "accuracy2": 96.61},
108
- "ํ™ํ•ฉ": {"model1": "Naive", "accuracy1": 99.86, "model2": "SeasonalNaive", "accuracy2": 98.56},
109
- }
110
-
111
- # ๊ธฐํƒ€ ํ’ˆ๋ชฉ์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ๋ชจ๋ธ (๋ฆฌ์ŠคํŠธ์— ์—†๋Š” ํ’ˆ๋ชฉ)
112
- default_models = {
113
- "model1": "SARIMA(1,0,1)(1,0,1,12)",
114
- "accuracy1": 99.0,
115
- "model2": "ETS(Multiplicative)",
116
- "accuracy2": 98.0
117
- }
118
-
119
- # -------------------------------------------------
120
- # UTILITIES ---------------------------------------
121
- # -------------------------------------------------
122
- DATE_CANDIDATES = {"date", "ds", "ymd", "๋‚ ์งœ", "prce_reg_mm", "etl_ldg_dt"}
123
- ITEM_CANDIDATES = {"item", "ํ’ˆ๋ชฉ", "code", "category", "pdlt_nm", "spcs_nm"}
124
- PRICE_CANDIDATES = {"price", "y", "value", "๊ฐ€๊ฒฉ", "avrg_prce"}
125
-
126
- def _standardize_columns(df: pd.DataFrame) -> pd.DataFrame:
127
- """Standardize column names to date/item/price and deduplicate."""
128
- col_map = {}
129
- for c in df.columns:
130
- lc = c.lower()
131
- if lc in DATE_CANDIDATES:
132
- col_map[c] = "date"
133
- elif lc in PRICE_CANDIDATES:
134
- col_map[c] = "price"
135
- elif lc in ITEM_CANDIDATES:
136
- # first hit as item, second as species
137
- if "item" not in col_map.values():
138
- col_map[c] = "item"
139
- else:
140
- col_map[c] = "species"
141
- df = df.rename(columns=col_map)
142
-
143
- # โ”€โ”€ handle duplicated columns after rename โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
144
- if df.columns.duplicated().any():
145
- df = df.loc[:, ~df.columns.duplicated()]
146
-
147
- # โ”€โ”€ index datetime to column โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
148
- if "date" not in df.columns and df.index.dtype.kind == "M":
149
- df.reset_index(inplace=True)
150
- df.rename(columns={df.columns[0]: "date"}, inplace=True)
151
-
152
- # โ”€โ”€ convert YYYYMM string to datetime โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
153
- if "date" in df.columns and pd.api.types.is_object_dtype(df["date"]):
154
- if len(df) > 0:
155
- # ๋” ์œ ์—ฐํ•œ ๋‚ ์งœ ๋ณ€ํ™˜
156
- try:
157
- # ์ƒ˜ํ”Œ ํ™•์ธ (๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ)
158
- sample = str(df["date"].iloc[0])
159
-
160
- # YYYYMM ํ˜•์‹ (6์ž๋ฆฌ)
161
- if sample.isdigit() and len(sample) == 6:
162
- df["date"] = pd.to_datetime(df["date"].astype(str), format="%Y%m", errors="coerce")
163
- df["date"] = df["date"] + pd.offsets.MonthEnd(0) # ํ•ด๋‹น ์›”์˜ ๋งˆ์ง€๋ง‰ ๋‚ ๋กœ ์„ค์ •
164
-
165
- # YYYYMMDD ํ˜•์‹ (8์ž๋ฆฌ)
166
- elif sample.isdigit() and len(sample) == 8:
167
- df["date"] = pd.to_datetime(df["date"].astype(str), format="%Y%m%d", errors="coerce")
168
-
169
- # ๊ธฐํƒ€ ํ˜•์‹์€ ์ž๋™ ๊ฐ์ง€
170
- else:
171
- df["date"] = pd.to_datetime(df["date"], errors="coerce")
172
- except:
173
- # ์‹คํŒจ ์‹œ ์ผ๋ฐ˜ ๋ณ€ํ™˜ ์‹œ๋„
174
- df["date"] = pd.to_datetime(df["date"], errors="coerce")
175
-
176
- # โ”€โ”€ build item from pdlt_nm + spcs_nm if needed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
177
- if "item" not in df.columns and {"pdlt_nm", "spcs_nm"}.issubset(df.columns):
178
- df["item"] = df["pdlt_nm"].str.strip() + "-" + df["spcs_nm"].str.strip()
179
-
180
- # โ”€โ”€ merge item + species โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
181
- if {"item", "species"}.issubset(df.columns):
182
- df["item"] = df["item"].astype(str).str.strip() + "-" + df["species"].astype(str).str.strip()
183
- df.drop(columns=["species"], inplace=True)
184
-
185
- return df
186
-
187
- @st.cache_data(show_spinner=False)
188
- def load_data() -> pd.DataFrame:
189
- """Load price data from CSV file."""
190
  try:
191
- if not CSV_PATH.exists():
192
- st.error(f"๐Ÿ’พ {CSV_PATH} ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
193
- st.stop()
194
-
195
- # CSV ํŒŒ์ผ ์ง์ ‘ ๋กœ๋“œ
196
- df = pd.read_csv(CSV_PATH)
197
- st.sidebar.success(f"CSV ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ: {len(df)}๊ฐœ ํ–‰")
198
-
199
- # ๋ฐ์ดํ„ฐ ํ‘œ์ค€ํ™” ์ „ ์›๋ณธ ๋ฐ์ดํ„ฐ ํ˜•ํƒœ ํ™•์ธ
200
- st.sidebar.write("์›๋ณธ ๋ฐ์ดํ„ฐ ์ปฌ๋Ÿผ:", list(df.columns))
201
 
202
- # ํ‘œ์ค€ํ™” ์ „ ์ƒ์„ธ ๋กœ๊ทธ
203
- before_std = len(df)
204
- df = _standardize_columns(df)
205
- after_std = len(df)
206
- if before_std != after_std:
207
- st.sidebar.warning(f"ํ‘œ์ค€ํ™” ์ค‘ {before_std - after_std}๊ฐœ ํ–‰์ด ์ œ์™ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
208
 
209
- # ํ‘œ์ค€ํ™” ํ›„ ๋กœ๊ทธ
210
- st.sidebar.write("ํ‘œ์ค€ํ™” ํ›„ ์ปฌ๋Ÿผ:", list(df.columns))
211
-
212
- # ํ•„์ˆ˜ ์ปฌ๋Ÿผ ํ™•์ธ
213
- missing = {c for c in ["date", "item", "price"] if c not in df.columns}
214
- if missing:
215
- st.error(f"ํ•„์ˆ˜ ์ปฌ๋Ÿผ ๋ˆ„๋ฝ: {', '.join(missing)} โ€” ํŒŒ์ผ ์ปฌ๋Ÿผ๋ช…์„ ํ™•์ธํ•˜์„ธ์š”.")
216
- st.stop()
217
-
218
- # ๋‚ ์งœ ๋ฐ์ดํ„ฐ ํ™•์ธ
219
- st.sidebar.write("๋‚ ์งœ ์ปฌ๋Ÿผ ๋ฐ์ดํ„ฐ ์ƒ˜ํ”Œ:", df["date"].head().tolist())
220
 
221
- # ๋‚ ์งœ ๋ณ€ํ™˜ ์ „ํ›„ ๋ฐ์ดํ„ฐ ์ˆ˜ ํ™•์ธ
222
- before_date_convert = len(df)
223
 
224
- # YYYYMM ํ˜•์‹ ๋ณ€ํ™˜ (์ˆซ์ž๋กœ ์ €์žฅ๋œ ๊ฒฝ์šฐ๋„ ์ฒ˜๋ฆฌ)
225
  try:
226
- # ๋ฐ์ดํ„ฐ ํƒ€์ž… ํ™•์ธ
227
- if pd.api.types.is_integer_dtype(df["date"]):
228
- # ์ •์ˆ˜ํ˜• YYYYMM์„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ ํ›„ ์ฒ˜๋ฆฌ
229
- df["date"] = df["date"].astype(str)
230
-
231
- # ๋ฌธ์ž์—ด ํ˜•์‹ ์ฒ˜๋ฆฌ
232
- if pd.api.types.is_object_dtype(df["date"]):
233
- # YYYYMM ํ˜•์‹์ธ์ง€ ํ™•์ธ (6์ž๋ฆฌ ์ˆซ์ž)
234
- if df["date"].str.match(r'^\d{6}$').all():
235
- # ์—ฐ, ์›” ๊ตฌ๋ถ„ํ•ด์„œ datetime์œผ๋กœ ๋ณ€ํ™˜
236
- df["year"] = df["date"].str[:4].astype(int)
237
- df["month"] = df["date"].str[4:6].astype(int)
238
- df["date"] = pd.to_datetime(dict(year=df["year"], month=df["month"], day=1))
239
- # ์›”์˜ ๋งˆ์ง€๋ง‰ ๋‚ ๋กœ ์„ค์ •
240
- df["date"] = df["date"] + pd.offsets.MonthEnd(0)
241
- # ์ž„์‹œ ์ปฌ๋Ÿผ ์‚ญ์ œ
242
- df.drop(columns=["year", "month"], inplace=True)
243
- else:
244
- # ์ผ๋ฐ˜ ๋ณ€ํ™˜ ์‹œ๋„
245
- df["date"] = pd.to_datetime(df["date"], errors="coerce")
246
- except Exception as e:
247
- st.sidebar.warning(f"๋‚ ์งœ ๋ณ€ํ™˜ ์˜ค๋ฅ˜: {str(e)}")
248
- # ์ตœํ›„์˜ ๋ฐฉ๋ฒ•์œผ๋กœ ๋‹ค์‹œ ์‹œ๋„
249
- try:
250
- df["date"] = pd.to_datetime(df["date"].astype(str), format="%Y%m", errors="coerce")
251
- df["date"] = df["date"] + pd.offsets.MonthEnd(0)
252
- except:
253
- df["date"] = pd.to_datetime(df["date"], errors="coerce")
254
-
255
- # ๋‚ ์งœ ๋ณ€ํ™˜ ํ›„ ๋ฐ์ดํ„ฐ ํ™•์ธ
256
- st.sidebar.write("๋‚ ์งœ ๋ณ€ํ™˜ ํ›„ ์ƒ˜ํ”Œ:", df["date"].head().tolist())
257
- after_date_convert = df.dropna(subset=["date"]).shape[0]
258
- if before_date_convert != after_date_convert:
259
- st.sidebar.warning(f"๋‚ ์งœ ๋ณ€ํ™˜ ์ค‘ {before_date_convert - after_date_convert}๊ฐœ ํ–‰์ด ์ œ์™ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
260
-
261
- # ๊ฐ€๊ฒฉ ๋ฐ์ดํ„ฐ ์ˆซ์ž๋กœ ๋ณ€ํ™˜
262
- df["price"] = pd.to_numeric(df["price"], errors="coerce")
263
-
264
- # NA ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์ „ํ›„ ์ˆ˜ ํ™•์ธ
265
- before_na_drop = len(df)
266
- df = df.dropna(subset=["date", "item", "price"])
267
- after_na_drop = len(df)
268
- if before_na_drop != after_na_drop:
269
- st.sidebar.warning(f"NA ์ œ๊ฑฐ ์ค‘ {before_na_drop - after_na_drop}๊ฐœ ํ–‰์ด ์ œ์™ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
270
-
271
- # ๊ฒฐ๊ณผ ์ •๋ ฌ
272
- df.sort_values("date", inplace=True)
273
-
274
- # ๋ฐ์ดํ„ฐ ์ •๋ณด ํ‘œ์‹œ
275
- if len(df) > 0:
276
- st.sidebar.write(f"์ตœ์ข… ๋ฐ์ดํ„ฐ: {len(df)}๊ฐœ ํ–‰")
277
- # datetime ํ˜•์‹์ธ์ง€ ํ™•์ธ
278
- if pd.api.types.is_datetime64_dtype(df["date"]):
279
- st.sidebar.write(f"๋ฐ์ดํ„ฐ ๋‚ ์งœ ๋ฒ”์œ„: {df['date'].min().strftime('%Y-%m-%d')} ~ {df['date'].max().strftime('%Y-%m-%d')}")
280
- else:
281
- st.sidebar.write(f"๋ฐ์ดํ„ฐ ๋‚ ์งœ ๋ฒ”์œ„: ๋‚ ์งœ ํ˜•์‹ ๋ณ€ํ™˜ ์‹คํŒจ. ํ˜„์žฌ ํ˜•์‹: {type(df['date'].iloc[0])}")
282
- st.sidebar.write(f"์ด ํ’ˆ๋ชฉ ์ˆ˜: {df['item'].nunique()}")
283
- st.sidebar.write(f"ํ’ˆ๋ชฉ๋ณ„ ํ‰๊ท  ๋ฐ์ดํ„ฐ ์ˆ˜: {len(df)/df['item'].nunique():.1f}๊ฐœ")
284
- else:
285
- st.error("์œ ํšจํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค!")
286
-
287
- return df
288
- except Exception as e:
289
- st.error(f"๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
290
- import traceback
291
- st.code(traceback.format_exc())
292
- st.stop()
293
-
294
- @st.cache_data(show_spinner=False)
295
- def get_items(df: pd.DataFrame):
296
- return sorted(df["item"].unique())
297
-
298
- def get_best_model_for_item(item):
299
- """ํ’ˆ๋ชฉ์— ๋งž๋Š” ์ตœ์  ๋ชจ๋ธ ์ •๋ณด ๋ฐ˜ํ™˜"""
300
- return item_models.get(item, default_models)
301
-
302
- def format_currency(value):
303
- """์›ํ™” ํ˜•์‹์œผ๋กœ ์ˆซ์ž ํฌ๋งทํŒ…"""
304
- if pd.isna(value) or not np.isfinite(value):
305
- return "N/A"
306
- return f"{value:,.0f}์›"
307
-
308
- # -------------------------------------------------
309
- # ๋ชจ๋ธ ๊ตฌํ˜„๋ถ€ --------------------------------------
310
- # -------------------------------------------------
311
- @st.cache_data(show_spinner=False, ttl=3600)
312
- def prepare_monthly_data(df):
313
- """์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„"""
314
- # ์›”๋ณ„๋กœ ์ง‘๊ณ„
315
- monthly_df = df.copy()
316
- monthly_df['year_month'] = monthly_df['date'].dt.strftime('%Y-%m')
317
- monthly_df = monthly_df.groupby('year_month').agg({'date': 'last', 'price': 'mean'}).reset_index(drop=True)
318
- monthly_df.sort_values('date', inplace=True)
319
-
320
- # ์ธ๋ฑ์Šค ์„ค์ •
321
- monthly_df.set_index('date', inplace=True)
322
-
323
- # ๊ฒฐ์ธก์น˜ ๋ณด๊ฐ„ (์›”๋ณ„ ๋ฐ์ดํ„ฐ์— ๋นˆ ์›”์ด ์žˆ์„ ์ˆ˜ ์žˆ์Œ)
324
- if len(monthly_df) > 1:
325
- monthly_df = monthly_df.asfreq('M', method='ffill')
326
-
327
- return monthly_df
328
-
329
- def fit_sarima(df, order, seasonal_order, horizon_end):
330
- """SARIMA ๋ชจ๋ธ ๊ตฌํ˜„"""
331
- import pandas as pd
332
- import numpy as np
333
- from statsmodels.tsa.statespace.sarimax import SARIMAX
334
-
335
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
336
- monthly_df = prepare_monthly_data(df)
337
-
338
- # ๋ชจ๋ธ ํ•™์Šต
339
- try:
340
- model = SARIMAX(
341
- monthly_df['price'],
342
- order=order,
343
- seasonal_order=seasonal_order,
344
- enforce_stationarity=False,
345
- enforce_invertibility=False
346
- )
347
- results = model.fit(disp=False)
348
-
349
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
350
- last_date = monthly_df.index[-1]
351
- end_date = pd.Timestamp(horizon_end)
352
- periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
353
-
354
- # ์˜ˆ์ธก ์ˆ˜ํ–‰
355
- forecast = results.get_forecast(steps=periods)
356
- pred_mean = forecast.predicted_mean
357
- pred_ci = forecast.conf_int()
358
-
359
- # Prophet ํ˜•์‹๏ฟฝ๏ฟฝ๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
360
- future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
361
-
362
- fc_df = pd.DataFrame({
363
- 'ds': future_dates,
364
- 'yhat': pred_mean.values,
365
- 'yhat_lower': pred_ci.iloc[:, 0].values,
366
- 'yhat_upper': pred_ci.iloc[:, 1].values
367
- })
368
-
369
- # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜ (๋‚ ์งœ, ๊ฐ€๊ฒฉ)
370
- fc_df_monthly = pd.DataFrame({
371
- 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
372
- })
373
-
374
- # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
375
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
376
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
377
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
378
-
379
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
380
- fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
381
- fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
382
- fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
383
-
384
- # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
385
- fc_df_monthly['yearly'] = 0
386
- fc_df_monthly['trend'] = 0
387
-
388
- try:
389
- # ๊ฐ€๋Šฅํ•˜๋ฉด ๊ณ„์ ˆ์„ฑ ๋ถ„ํ•ด
390
- decomposition = seasonal_decompose(monthly_df['price'], model='multiplicative', period=12)
391
- trend = decomposition.trend
392
- seasonal = decomposition.seasonal
393
-
394
- # ๊ฒฐ๊ณผ์— ๊ณ„์ ˆ์„ฑ ๋ฐ˜์˜
395
- for i, date in enumerate(fc_df_monthly['ds']):
396
- month = date.month
397
- if month in seasonal.index.month:
398
- seasonal_value = seasonal[seasonal.index.month == month].mean()
399
- fc_df_monthly.loc[i, 'yearly'] = seasonal_value
400
  except:
401
  pass
402
-
403
- return fc_df_monthly
404
-
405
- except Exception as e:
406
- st.error(f"SARIMA ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
407
- return None
408
-
409
- def fit_ets(df, seasonal_type, horizon_end):
410
- """ETS ๋ชจ๋ธ ๊ตฌํ˜„"""
411
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
412
- monthly_df = prepare_monthly_data(df)
413
-
414
- # ๋ชจ๋ธ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
415
- if seasonal_type == 'multiplicative':
416
- trend_type = 'add'
417
- seasonal = 'mul'
418
- else: # additive
419
- trend_type = 'add'
420
- seasonal = 'add'
421
-
422
- # ๋ชจ๋ธ ํ•™์Šต
423
- try:
424
- model = ExponentialSmoothing(
425
- monthly_df['price'],
426
- trend=trend_type,
427
- seasonal=seasonal,
428
- seasonal_periods=12,
429
- damped=True
430
- )
431
- results = model.fit(optimized=True)
432
-
433
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
434
- last_date = monthly_df.index[-1]
435
- end_date = pd.Timestamp(horizon_end)
436
- periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
437
-
438
- # ์˜ˆ์ธก ์ˆ˜ํ–‰
439
- forecast = results.forecast(periods)
440
-
441
- # Prophet ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
442
- future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
443
-
444
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ • (ETS๋Š” ๊ธฐ๋ณธ ์‹ ๋ขฐ ๊ตฌ๊ฐ„์„ ์ œ๊ณตํ•˜์ง€ ์•Š์Œ)
445
- std_error = np.std(results.resid)
446
- lower_bound = forecast - 1.96 * std_error
447
- upper_bound = forecast + 1.96 * std_error
448
-
449
- fc_df = pd.DataFrame({
450
- 'ds': future_dates,
451
- 'yhat': forecast.values,
452
- 'yhat_lower': lower_bound.values,
453
- 'yhat_upper': upper_bound.values
454
- })
455
-
456
- # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
457
- fc_df_monthly = pd.DataFrame({
458
- 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
459
- })
460
-
461
- # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
462
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
463
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
464
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
465
-
466
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
467
- fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
468
- fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
469
- fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
470
-
471
- # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
472
- fc_df_monthly['yearly'] = 0
473
- fc_df_monthly['trend'] = 0
474
-
475
- try:
476
- # ๊ฐ€๋Šฅํ•˜๋ฉด ๊ณ„์ ˆ์„ฑ ๋ถ„ํ•ด
477
- decomposition = seasonal_decompose(monthly_df['price'], model=seasonal_type, period=12)
478
- trend = decomposition.trend
479
- seasonal = decomposition.seasonal
480
 
481
- # ๊ฒฐ๊ณผ์— ๊ณ„์ ˆ์„ฑ ๋ฐ˜์˜
482
- for i, date in enumerate(fc_df_monthly['ds']):
483
- month = date.month
484
- if month in seasonal.index.month:
485
- seasonal_value = seasonal[seasonal.index.month == month].mean()
486
- fc_df_monthly.loc[i, 'yearly'] = seasonal_value
487
- except:
488
- pass
489
-
490
- return fc_df_monthly
491
-
492
- except Exception as e:
493
- st.error(f"ETS ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
494
- return None
495
-
496
- def fit_holt(df, horizon_end):
497
- """Holt ๋ชจ๋ธ ๊ตฌํ˜„"""
498
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
499
- monthly_df = prepare_monthly_data(df)
500
-
501
- # ๋ชจ๋ธ ํ•™์Šต
502
- try:
503
- model = Holt(monthly_df['price'], damped=True)
504
- results = model.fit(optimized=True)
505
-
506
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
507
- last_date = monthly_df.index[-1]
508
- end_date = pd.Timestamp(horizon_end)
509
- periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
510
-
511
- # ์˜ˆ์ธก ์ˆ˜ํ–‰
512
- forecast = results.forecast(periods)
513
-
514
- # Prophet ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
515
- future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
516
-
517
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
518
- std_error = np.std(results.resid)
519
- lower_bound = forecast - 1.96 * std_error
520
- upper_bound = forecast + 1.96 * std_error
521
-
522
- fc_df = pd.DataFrame({
523
- 'ds': future_dates,
524
- 'yhat': forecast.values,
525
- 'yhat_lower': lower_bound.values,
526
- 'yhat_upper': upper_bound.values
527
- })
528
-
529
- # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
530
- fc_df_monthly = pd.DataFrame({
531
- 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
532
- })
533
-
534
- # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
535
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
536
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
537
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
538
-
539
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
540
- fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
541
- fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
542
- fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
543
-
544
- # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
545
- fc_df_monthly['yearly'] = 0
546
- fc_df_monthly['trend'] = fc_df_monthly['yhat'] # Holt๋Š” ์ถ”์„ธ๋งŒ ๋ชจ๋ธ๋ง
547
-
548
- return fc_df_monthly
549
-
550
- except Exception as e:
551
- st.error(f"Holt ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
552
- return None
553
-
554
- def fit_holt_winters(df, horizon_end):
555
- """Holt-Winters ๋ชจ๋ธ ๊ตฌํ˜„"""
556
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
557
- monthly_df = prepare_monthly_data(df)
558
-
559
- # ๋ชจ๋ธ ํ•™์Šต
560
- try:
561
- model = ExponentialSmoothing(
562
- monthly_df['price'],
563
- trend='add',
564
- seasonal='mul', # ๊ณ„์ ˆ์„ฑ์€ ๊ณฑ์…ˆ ๋ฐฉ์‹์ด ๋†์‚ฐ๋ฌผ ๊ฐ€๊ฒฉ์— ๋” ์ ํ•ฉ
565
- seasonal_periods=12,
566
- damped=True
567
- )
568
- results = model.fit(optimized=True)
569
-
570
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
571
- last_date = monthly_df.index[-1]
572
- end_date = pd.Timestamp(horizon_end)
573
- periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
574
-
575
- # ์˜ˆ์ธก ์ˆ˜ํ–‰
576
- forecast = results.forecast(periods)
577
-
578
- # Prophet ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
579
- future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
580
-
581
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
582
- std_error = np.std(results.resid)
583
- lower_bound = forecast - 1.96 * std_error
584
- upper_bound = forecast + 1.96 * std_error
585
-
586
- fc_df = pd.DataFrame({
587
- 'ds': future_dates,
588
- 'yhat': forecast.values,
589
- 'yhat_lower': lower_bound.values,
590
- 'yhat_upper': upper_bound.values
591
- })
592
-
593
- # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
594
- fc_df_monthly = pd.DataFrame({
595
- 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
596
- })
597
-
598
- # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
599
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
600
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
601
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
602
-
603
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
604
- fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
605
- fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
606
- fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
607
-
608
- # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
609
- fc_df_monthly['yearly'] = 0
610
- fc_df_monthly['trend'] = 0
611
-
612
- try:
613
- # Holt-Winters ๋ชจ๋ธ์—์„œ ๊ณ„์ ˆ์„ฑ ์ถ”์ถœ
614
- seasonal = results.seasonal_
615
-
616
- # ๊ฒฐ๊ณผ์— ๊ณ„์ ˆ์„ฑ ๋ฐ˜์˜
617
- for i, date in enumerate(fc_df_monthly['ds']):
618
- month = date.month - 1 # 0-indexed
619
- if month < len(seasonal):
620
- fc_df_monthly.loc[i, 'yearly'] = seasonal[month] * fc_df_monthly.loc[i, 'yhat']
621
- fc_df_monthly.loc[i, 'trend'] = fc_df_monthly.loc[i, 'yhat'] - fc_df_monthly.loc[i, 'yearly']
622
- except:
623
- pass
624
-
625
- return fc_df_monthly
626
-
627
- except Exception as e:
628
- st.error(f"Holt-Winters ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
629
- return None
630
-
631
- def fit_moving_average(df, window, horizon_end):
632
- """์ด๋™ ํ‰๊ท  ๋ชจ๋ธ ๊ตฌํ˜„"""
633
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
634
- monthly_df = prepare_monthly_data(df)
635
-
636
- try:
637
- # ๋งˆ์ง€๋ง‰ window ๊ฐœ์›”์˜ ํ‰๊ท  ๊ณ„์‚ฐ
638
- last_values = monthly_df['price'].iloc[-window:]
639
- ma_value = last_values.mean()
640
-
641
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
642
- last_date = monthly_df.index[-1]
643
- end_date = pd.Timestamp(horizon_end)
644
- periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
645
-
646
- # ์˜ˆ์ธก ์ˆ˜ํ–‰ (๋ชจ๋“  ๋ฏธ๋ž˜ ์‹œ์ ์— ๋™์ผํ•œ ๊ฐ’)
647
- future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
648
-
649
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
650
- std_error = last_values.std()
651
- lower_bound = ma_value - 1.96 * std_error
652
- upper_bound = ma_value + 1.96 * std_error
653
-
654
- fc_df = pd.DataFrame({
655
- 'ds': future_dates,
656
- 'yhat': [ma_value] * len(future_dates),
657
- 'yhat_lower': [lower_bound] * len(future_dates),
658
- 'yhat_upper': [upper_bound] * len(future_dates)
659
- })
660
-
661
- # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
662
- fc_df_monthly = pd.DataFrame({
663
- 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
664
- })
665
-
666
- # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
667
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
668
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
669
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
670
-
671
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
672
- fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
673
- fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
674
- fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
675
-
676
- # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
677
- fc_df_monthly['yearly'] = 0
678
- fc_df_monthly['trend'] = fc_df_monthly['yhat']
679
-
680
- return fc_df_monthly
681
-
682
  except Exception as e:
683
- st.error(f"์ด๋™ ํ‰๊ท  ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
684
- return None
685
-
686
- def fit_weighted_ma(df, window, horizon_end):
687
- """๊ฐ€์ค‘ ์ด๋™ ํ‰๊ท  ๋ชจ๋ธ ๊ตฌํ˜„"""
688
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
689
- monthly_df = prepare_monthly_data(df)
690
-
691
- try:
692
- # ๋งˆ์ง€๋ง‰ window ๊ฐœ์›”์˜ ๊ฐ€์ค‘ ํ‰๊ท  ๊ณ„์‚ฐ
693
- last_values = monthly_df['price'].iloc[-window:].to_numpy()
694
-
695
- # ๊ฐ€์ค‘์น˜ ์ƒ์„ฑ (์ตœ๊ทผ ๋ฐ์ดํ„ฐ์— ๋” ๋†’์€ ๊ฐ€์ค‘์น˜)
696
- weights = np.arange(1, window + 1)
697
- weights = weights / np.sum(weights)
698
-
699
- wma_value = np.sum(last_values * weights)
700
-
701
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
702
- last_date = monthly_df.index[-1]
703
- end_date = pd.Timestamp(horizon_end)
704
- periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
705
-
706
- # ์˜ˆ์ธก ์ˆ˜ํ–‰ (๋ชจ๋“  ๋ฏธ๋ž˜ ์‹œ์ ์— ๋™์ผํ•œ ๊ฐ’)
707
- future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
708
-
709
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
710
- std_error = np.std(last_values)
711
- lower_bound = wma_value - 1.96 * std_error
712
- upper_bound = wma_value + 1.96 * std_error
713
-
714
- fc_df = pd.DataFrame({
715
- 'ds': future_dates,
716
- 'yhat': [wma_value] * len(future_dates),
717
- 'yhat_lower': [lower_bound] * len(future_dates),
718
- 'yhat_upper': [upper_bound] * len(future_dates)
719
- })
720
-
721
- # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
722
- fc_df_monthly = pd.DataFrame({
723
- 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
724
- })
725
-
726
- # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
727
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
728
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
729
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
730
-
731
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
732
- fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
733
- fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
734
- fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
735
-
736
- # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
737
- fc_df_monthly['yearly'] = 0
738
- fc_df_monthly['trend'] = fc_df_monthly['yhat']
739
-
740
- return fc_df_monthly
741
-
742
- except Exception as e:
743
- st.error(f"๊ฐ€์ค‘ ์ด๋™ ํ‰๊ท  ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
744
- return None
745
-
746
- def fit_naive(df, horizon_end):
747
- """๋‹จ์ˆœ Naive ๋ชจ๋ธ ๊ตฌํ˜„"""
748
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
749
- monthly_df = prepare_monthly_data(df)
750
-
751
- try:
752
- # ๋งˆ์ง€๋ง‰ ๊ฐ’ ์‚ฌ์šฉ
753
- last_value = monthly_df['price'].iloc[-1]
754
-
755
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
756
- last_date = monthly_df.index[-1]
757
- end_date = pd.Timestamp(horizon_end)
758
- periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
759
-
760
- # ์˜ˆ์ธก ์ˆ˜ํ–‰ (๋ชจ๋“  ๋ฏธ๋ž˜ ์‹œ์ ์— ๋งˆ์ง€๋ง‰ ๊ฐ’)
761
- future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
762
-
763
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ • (๊ณผ๊ฑฐ 12๊ฐœ์›” ํ‘œ์ค€ํŽธ์ฐจ ์‚ฌ์šฉ)
764
- history_std = monthly_df['price'].iloc[-12:].std() if len(monthly_df) >= 12 else monthly_df['price'].std()
765
- lower_bound = last_value - 1.96 * history_std
766
- upper_bound = last_value + 1.96 * history_std
767
-
768
- fc_df = pd.DataFrame({
769
- 'ds': future_dates,
770
- 'yhat': [last_value] * len(future_dates),
771
- 'yhat_lower': [lower_bound] * len(future_dates),
772
- 'yhat_upper': [upper_bound] * len(future_dates)
773
- })
774
-
775
- # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
776
- fc_df_monthly = pd.DataFrame({
777
- 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
778
- })
779
-
780
- # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
781
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
782
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
783
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
784
-
785
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
786
- fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
787
- fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
788
- fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
789
-
790
- # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
791
- fc_df_monthly['yearly'] = 0
792
- fc_df_monthly['trend'] = fc_df_monthly['yhat']
793
-
794
- return fc_df_monthly
795
-
796
- except Exception as e:
797
- st.error(f"Naive ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
798
- return None
799
-
800
- def fit_seasonal_naive(df, horizon_end):
801
- """๊ณ„์ ˆ์„ฑ Naive ๋ชจ๋ธ ๊ตฌํ˜„"""
802
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
803
- monthly_df = prepare_monthly_data(df)
804
-
805
- try:
806
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
807
- last_date = monthly_df.index[-1]
808
- end_date = pd.Timestamp(horizon_end)
809
- periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
810
-
811
- # ์˜ˆ์ธก ์ˆ˜ํ–‰ (๊ฐ ์›”์— ๋Œ€ํ•ด ์ž‘๋…„ ๊ฐ™์€ ๋‹ฌ ๊ฐ€๊ฒฉ ์‚ฌ์šฉ)
812
- future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
813
- future_values = []
814
- lower_bounds = []
815
- upper_bounds = []
816
-
817
- for date in future_dates:
818
- # ๊ฐ™์€ ์›”์˜ ๊ฐ’ ์ฐพ๊ธฐ
819
- same_month_values = monthly_df[monthly_df.index.month == date.month]['price']
820
-
821
- if len(same_month_values) > 0:
822
- # ๊ฐ™์€ ์›” ๊ฐ€์žฅ ์ตœ๊ทผ ๊ฐ’ ์‚ฌ์šฉ
823
- forecast_value = same_month_values.iloc[-1]
824
-
825
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„
826
- std_error = same_month_values.std() if len(same_month_values) > 1 else monthly_df['price'].std()
827
- lower_bound = forecast_value - 1.96 * std_error
828
- upper_bound = forecast_value + 1.96 * std_error
829
- else:
830
- # ๊ฐ™์€ ์›” ๋ฐ์ดํ„ฐ ์—†์œผ๋ฉด ์ „์ฒด ํ‰๊ท  ์‚ฌ์šฉ
831
- forecast_value = monthly_df['price'].mean()
832
- std_error = monthly_df['price'].std()
833
- lower_bound = forecast_value - 1.96 * std_error
834
- upper_bound = forecast_value + 1.96 * std_error
835
-
836
- future_values.append(forecast_value)
837
- lower_bounds.append(lower_bound)
838
- upper_bounds.append(upper_bound)
839
-
840
- fc_df = pd.DataFrame({
841
- 'ds': future_dates,
842
- 'yhat': future_values,
843
- 'yhat_lower': lower_bounds,
844
- 'yhat_upper': upper_bounds
845
- })
846
-
847
- # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
848
- fc_df_monthly = pd.DataFrame({
849
- 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
850
- })
851
-
852
- # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
853
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
854
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
855
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
856
-
857
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
858
- fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
859
- fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
860
- fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
861
-
862
- # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
863
- fc_df_monthly['yearly'] = fc_df_monthly['yhat']
864
- fc_df_monthly['trend'] = 0
865
-
866
- return fc_df_monthly
867
-
868
- except Exception as e:
869
- st.error(f"Seasonal Naive ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
870
- return None
871
-
872
- def fit_fourier_lr(df, horizon_end):
873
- """Fourier + ์„ ํ˜• ํšŒ๊ท€ ๋ชจ๋ธ ๊ตฌํ˜„"""
874
- from sklearn.linear_model import LinearRegression
875
-
876
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
877
- monthly_df = prepare_monthly_data(df)
878
-
879
- try:
880
- # ์‹œ๊ฐ„ ๋ณ€์ˆ˜ ์ƒ์„ฑ
881
- y = monthly_df['price'].values
882
- t = np.arange(len(y))
883
-
884
- # Fourier ํŠน์„ฑ ์ƒ์„ฑ (์—ฐ๊ฐ„ ๊ณ„์ ˆ์„ฑ)
885
- p = 12 # ์ฃผ๊ธฐ (1๋…„)
886
- X = np.column_stack([
887
- t, # ์„ ํ˜• ์ถ”์„ธ
888
- np.sin(2 * np.pi * t / p),
889
- np.cos(2 * np.pi * t / p),
890
- np.sin(4 * np.pi * t / p),
891
- np.cos(4 * np.pi * t / p)
892
- ])
893
-
894
- # ๋ชจ๋ธ ํ•™์Šต
895
- model = LinearRegression()
896
- model.fit(X, y)
897
-
898
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
899
- last_date = monthly_df.index[-1]
900
- end_date = pd.Timestamp(horizon_end)
901
- periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
902
-
903
- # ์˜ˆ์ธก ์ˆ˜ํ–‰
904
- future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
905
-
906
- # ๋ฏธ๋ž˜ ์‹œ์  ํŠน์„ฑ ์ƒ์„ฑ
907
- t_future = np.arange(len(y), len(y) + periods)
908
- X_future = np.column_stack([
909
- t_future,
910
- np.sin(2 * np.pi * t_future / p),
911
- np.cos(2 * np.pi * t_future / p),
912
- np.sin(4 * np.pi * t_future / p),
913
- np.cos(4 * np.pi * t_future / p)
914
- ])
915
-
916
- # ์˜ˆ์ธก
917
- forecast = model.predict(X_future)
918
-
919
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
920
- y_pred = model.predict(X)
921
- mse = np.mean((y - y_pred) ** 2)
922
- std_error = np.sqrt(mse)
923
-
924
- lower_bound = forecast - 1.96 * std_error
925
- upper_bound = forecast + 1.96 * std_error
926
-
927
- fc_df = pd.DataFrame({
928
- 'ds': future_dates,
929
- 'yhat': forecast,
930
- 'yhat_lower': lower_bound,
931
- 'yhat_upper': upper_bound
932
- })
933
-
934
- # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
935
- fc_df_monthly = pd.DataFrame({
936
- 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
937
- })
938
-
939
- # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
940
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
941
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
942
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
943
-
944
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
945
- fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
946
- fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
947
- fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
948
-
949
- # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
950
- fc_df_monthly['trend'] = model.coef_[0] * np.arange(len(fc_df_monthly)) + model.intercept_
951
-
952
- # ๊ณ„์ ˆ์„ฑ ๊ณ„์‚ฐ
953
- season_features = np.column_stack([
954
- np.sin(2 * np.pi * np.arange(len(fc_df_monthly)) / p),
955
- np.cos(2 * np.pi * np.arange(len(fc_df_monthly)) / p),
956
- np.sin(4 * np.pi * np.arange(len(fc_df_monthly)) / p),
957
- np.cos(4 * np.pi * np.arange(len(fc_df_monthly)) / p)
958
- ])
959
-
960
- seasonal_effect = np.dot(season_features, model.coef_[1:5])
961
- fc_df_monthly['yearly'] = seasonal_effect
962
-
963
- return fc_df_monthly
964
-
965
- except Exception as e:
966
- st.error(f"Fourier + LR ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
967
- return None
968
-
969
- def fit_linear_trend(df, horizon_end):
970
- """์„ ํ˜• ์ถ”์„ธ ๋ชจ๋ธ ๊ตฌํ˜„"""
971
- from sklearn.linear_model import LinearRegression
972
-
973
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
974
- monthly_df = prepare_monthly_data(df)
975
-
976
- try:
977
- # ์‹œ๊ฐ„ ๋ณ€์ˆ˜ ์ƒ์„ฑ
978
- y = monthly_df['price'].values
979
- t = np.arange(len(y)).reshape(-1, 1)
980
-
981
- # ๋ชจ๋ธ ํ•™์Šต
982
- model = LinearRegression()
983
- model.fit(t, y)
984
-
985
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
986
- last_date = monthly_df.index[-1]
987
- end_date = pd.Timestamp(horizon_end)
988
- periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
989
-
990
- # ์˜ˆ์ธก ์ˆ˜ํ–‰
991
- future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
992
- t_future = np.arange(len(y), len(y) + periods).reshape(-1, 1)
993
- forecast = model.predict(t_future)
994
-
995
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
996
- y_pred = model.predict(t)
997
- mse = np.mean((y - y_pred) ** 2)
998
- std_error = np.sqrt(mse)
999
-
1000
- lower_bound = forecast - 1.96 * std_error
1001
- upper_bound = forecast + 1.96 * std_error
1002
-
1003
- fc_df = pd.DataFrame({
1004
- 'ds': future_dates,
1005
- 'yhat': forecast,
1006
- 'yhat_lower': lower_bound,
1007
- 'yhat_upper': upper_bound
1008
- })
1009
-
1010
- # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
1011
- fc_df_monthly = pd.DataFrame({
1012
- 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
1013
- })
1014
-
1015
- # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
1016
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
1017
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
1018
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
1019
-
1020
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
1021
- fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
1022
- fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
1023
- fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
1024
-
1025
- # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
1026
- fc_df_monthly['yearly'] = 0
1027
- fc_df_monthly['trend'] = fc_df_monthly['yhat']
1028
-
1029
- return fc_df_monthly
1030
-
1031
- except Exception as e:
1032
- st.error(f"Linear Trend ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
1033
- return None
1034
-
1035
- def fit_simple_exp_smoothing(df, horizon_end):
1036
- """๋‹จ์ˆœ ์ง€์ˆ˜ ํ‰ํ™œ ๋ชจ๋ธ ๊ตฌํ˜„"""
1037
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
1038
- monthly_df = prepare_monthly_data(df)
1039
-
1040
- try:
1041
- # ๋ชจ๋ธ ํ•™์Šต
1042
- model = SimpleExpSmoothing(monthly_df['price'])
1043
- results = model.fit(optimized=True)
1044
-
1045
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ๊ณ„์‚ฐ
1046
- last_date = monthly_df.index[-1]
1047
- end_date = pd.Timestamp(horizon_end)
1048
- periods = (end_date.year - last_date.year) * 12 + (end_date.month - last_date.month)
1049
-
1050
- # ์˜ˆ์ธก ์ˆ˜ํ–‰
1051
- forecast = results.forecast(periods)
1052
-
1053
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”์ •
1054
- std_error = np.std(results.resid)
1055
- lower_bound = forecast - 1.96 * std_error
1056
- upper_bound = forecast + 1.96 * std_error
1057
-
1058
- # Prophet ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
1059
- future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=periods, freq='M')
1060
-
1061
- fc_df = pd.DataFrame({
1062
- 'ds': future_dates,
1063
- 'yhat': forecast.values,
1064
- 'yhat_lower': lower_bound.values,
1065
- 'yhat_upper': upper_bound.values
1066
- })
1067
-
1068
- # ์›”๋ณ„๋กœ ๊ฒฐ๊ณผ ๋ณ€ํ™˜
1069
- fc_df_monthly = pd.DataFrame({
1070
- 'ds': pd.date_range(start=monthly_df.index[0], end=future_dates[-1], freq='M'),
1071
- })
1072
-
1073
- # ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
1074
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat'] = monthly_df['price'].values
1075
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_lower'] = monthly_df['price'].values
1076
- fc_df_monthly.loc[:len(monthly_df)-1, 'yhat_upper'] = monthly_df['price'].values
1077
-
1078
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„์˜ ๊ฒฐ๊ณผ ์ถ”๊ฐ€
1079
- fc_df_monthly.loc[len(monthly_df):, 'yhat'] = fc_df['yhat'].values
1080
- fc_df_monthly.loc[len(monthly_df):, 'yhat_lower'] = fc_df['yhat_lower'].values
1081
- fc_df_monthly.loc[len(monthly_df):, 'yhat_upper'] = fc_df['yhat_upper'].values
1082
-
1083
- # yearly, trend ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Prophet ํ˜ธํ™˜)
1084
- fc_df_monthly['yearly'] = 0
1085
- fc_df_monthly['trend'] = fc_df_monthly['yhat']
1086
-
1087
- return fc_df_monthly
1088
-
1089
- except Exception as e:
1090
- st.error(f"Simple Exponential Smoothing ๋ชจ๋ธ ์˜ค๋ฅ˜: {str(e)}")
1091
- return None
1092
-
1093
- @st.cache_data(show_spinner=False, ttl=3600)
1094
- def fit_optimal_model(df, item_name, horizon_end, model_type="primary"):
1095
- """ํ’ˆ๋ชฉ๋ณ„ ์ตœ์  ๋ชจ๋ธ ์ ์šฉ"""
1096
- # ๋ฐ์ดํ„ฐ ์ค€๋น„ ๋ฐ ์ •๋ฆฌ
1097
- df = df.copy()
1098
- df = df.dropna(subset=["date", "price"])
1099
-
1100
- # ํ’ˆ๋ชฉ๋ณ„ ์ตœ์  ๋ชจ๋ธ ์„ ํƒ
1101
- model_info = get_best_model_for_item(item_name)
1102
-
1103
- if model_type == "primary":
1104
- model_name = model_info["model1"]
1105
- accuracy = model_info["accuracy1"]
1106
- else: # backup
1107
- model_name = model_info["model2"]
1108
- accuracy = model_info["accuracy2"]
1109
-
1110
- st.info(f"{item_name}์— ์ตœ์ ํ™”๋œ {model_name} ๋ชจ๋ธ ์ ์šฉ (์ •ํ™•๋„: {accuracy}%)")
1111
-
1112
- # ํŠน์ˆ˜ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ํ’ˆ๋ชฉ ํ™•์ธ
1113
- needs_monitoring = "special" in model_info and model_info["special"] == "accuracy_drop"
1114
- if needs_monitoring:
1115
- st.warning(f"โš ๏ธ {item_name}๋Š” ํŠน์ • ์›”์— ์ •ํ™•๋„๊ฐ€ ๊ธ‰๋ฝํ•  ์ˆ˜ ์žˆ๋Š” ํ’ˆ๋ชฉ์ž…๋‹ˆ๋‹ค. ์˜ˆ์ธก ๊ฒฐ๊ณผ๋ฅผ ์ฃผ์˜ ๊นŠ๊ฒŒ ์‚ดํŽด๋ณด์„ธ์š”.")
1116
-
1117
- # ๋ชจ๋ธ ์„ ํƒ ๋ฐ ํ•™์Šต
1118
- if "SARIMA(1,0,1)(1,0,1,12)" in model_name:
1119
- return fit_sarima(df, order=(1,0,1), seasonal_order=(1,0,1,12), horizon_end=horizon_end)
1120
- elif "SARIMA(1,1,1)(1,1,1,12)" in model_name:
1121
- return fit_sarima(df, order=(1,1,1), seasonal_order=(1,1,1,12), horizon_end=horizon_end)
1122
- elif "SARIMA(0,1,1)(0,1,1,12)" in model_name:
1123
- return fit_sarima(df, order=(0,1,1), seasonal_order=(0,1,1,12), horizon_end=horizon_end)
1124
- elif "ETS(Multiplicative)" in model_name:
1125
- return fit_ets(df, seasonal_type="multiplicative", horizon_end=horizon_end)
1126
- elif "ETS(Additive)" in model_name:
1127
- return fit_ets(df, seasonal_type="additive", horizon_end=horizon_end)
1128
- elif "Holt-Winters" in model_name:
1129
- return fit_holt_winters(df, horizon_end=horizon_end)
1130
- elif "Holt" in model_name:
1131
- return fit_holt(df, horizon_end=horizon_end)
1132
- elif "MovingAverage-6 m" in model_name:
1133
- return fit_moving_average(df, window=6, horizon_end=horizon_end)
1134
- elif "WeightedMA-6 m" in model_name:
1135
- return fit_weighted_ma(df, window=6, horizon_end=horizon_end)
1136
- elif "Naive" in model_name and "Seasonal" not in model_name:
1137
- return fit_naive(df, horizon_end=horizon_end)
1138
- elif "SeasonalNaive" in model_name:
1139
- return fit_seasonal_naive(df, horizon_end=horizon_end)
1140
- elif "Fourier + LR" in model_name:
1141
- return fit_fourier_lr(df, horizon_end=horizon_end)
1142
- elif "LinearTrend" in model_name:
1143
- return fit_linear_trend(df, horizon_end=horizon_end)
1144
- elif "SimpleExpSmoothing" in model_name:
1145
- return fit_simple_exp_smoothing(df, horizon_end=horizon_end)
1146
- else:
1147
- st.warning(f"์•Œ ์ˆ˜ ์—†๋Š” ๋ชจ๋ธ: {model_name}. ๊ธฐ๋ณธ ๋ชจ๋ธ(SARIMA)์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
1148
- return fit_sarima(df, order=(1,0,1), seasonal_order=(1,0,1,12), horizon_end=horizon_end)
1149
-
1150
- def fit_ensemble_model(df, item_name, horizon_end):
1151
- """1์œ„์™€ 2์œ„ ๋ชจ๋ธ์˜ ์•™์ƒ๋ธ” ์ˆ˜ํ–‰"""
1152
- # 1์œ„ ๋ชจ๋ธ ์˜ˆ์ธก
1153
- fc1 = fit_optimal_model(df, item_name, horizon_end, model_type="primary")
1154
-
1155
- # 2์œ„ ๋ชจ๋ธ ์˜ˆ์ธก
1156
- fc2 = fit_optimal_model(df, item_name, horizon_end, model_type="backup")
1157
-
1158
- # ๋‘ ๋ชจ๋ธ ๋ชจ๋‘ ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ๋งŒ ์•™์ƒ๋ธ”
1159
- if fc1 is not None and fc2 is not None:
1160
- # ์•™์ƒ๋ธ” ๊ฐ€์ค‘์น˜ ๊ณ„์‚ฐ (์ •ํ™•๋„ ๊ธฐ๋ฐ˜)
1161
- model_info = get_best_model_for_item(item_name)
1162
- acc1 = model_info["accuracy1"]
1163
- acc2 = model_info["accuracy2"]
1164
-
1165
- # ์ •ํ™•๋„ ์ฐจ์ด๊ฐ€ 0.2%p ์ด๋‚ด์ธ ๊ฒฝ์šฐ ์•™์ƒ๋ธ” ์ˆ˜ํ–‰
1166
- accuracy_diff = abs(acc1 - acc2)
1167
-
1168
- if accuracy_diff <= 0.2:
1169
- st.success(f"๋‘ ๋ชจ๋ธ์˜ ์ •ํ™•๋„ ์ฐจ์ด๊ฐ€ {accuracy_diff:.2f}%p๋กœ ์ž‘์•„ ์•™์ƒ๋ธ”์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.")
1170
-
1171
- # ์ •ํ™•๋„ ๊ธฐ๋ฐ˜ ๊ฐ€์ค‘์น˜ ๊ณ„์‚ฐ
1172
- total_acc = acc1 + acc2
1173
- w1 = acc1 / total_acc
1174
- w2 = acc2 / total_acc
1175
-
1176
- # ์•™์ƒ๋ธ” ๊ฒฐ๊ณผ ์ƒ์„ฑ
1177
- fc_ensemble = fc1.copy()
1178
- fc_ensemble['yhat'] = w1 * fc1['yhat'] + w2 * fc2['yhat']
1179
- fc_ensemble['yhat_lower'] = w1 * fc1['yhat_lower'] + w2 * fc2['yhat_lower']
1180
- fc_ensemble['yhat_upper'] = w1 * fc1['yhat_upper'] + w2 * fc2['yhat_upper']
1181
-
1182
- return fc_ensemble
1183
- else:
1184
- st.info(f"์ •ํ™•๋„ ์ฐจ์ด๊ฐ€ {accuracy_diff:.2f}%p๋กœ ์ปค์„œ 1์œ„ ๋ชจ๋ธ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
1185
- return fc1
1186
-
1187
- # ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•œ ๊ฒฝ์šฐ ์„ฑ๊ณตํ•œ ๋ชจ๋ธ ๋ฐ˜ํ™˜
1188
- return fc1 if fc1 is not None else fc2
1189
-
1190
- # -------------------------------------------------
1191
- # MAIN APP ---------------------------------------
1192
- # -------------------------------------------------
1193
- # ๋ฐ์ดํ„ฐ ๋กœ๋“œ
1194
- raw_df = load_data()
1195
-
1196
- if len(raw_df) == 0:
1197
- st.error("๋ฐ์ดํ„ฐ๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํŒŒ์ผ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")
1198
- st.stop()
1199
-
1200
- st.sidebar.header("๐Ÿ” ํ’ˆ๋ชฉ ์„ ํƒ")
1201
- selected_item = st.sidebar.selectbox("ํ’ˆ๋ชฉ", get_items(raw_df))
1202
- current_date = date.today()
1203
- st.sidebar.caption(f"์˜ค๋Š˜: {current_date}")
1204
-
1205
- # ์„ ํƒ๋œ ํ’ˆ๋ชฉ์˜ ์ตœ์  ๋ชจ๋ธ ์ •๋ณด ํ‘œ์‹œ
1206
- model_info = get_best_model_for_item(selected_item)
1207
- st.sidebar.subheader("ํ’ˆ๋ชฉ๋ณ„ ์ตœ์  ๋ชจ๋ธ")
1208
- st.sidebar.markdown(f"**1์œ„ ๋ชจ๋ธ:** {model_info['model1']} (์ •ํ™•๋„: {model_info['accuracy1']}%)")
1209
- st.sidebar.markdown(f"**2์œ„ ๋ชจ๋ธ:** {model_info['model2']} (์ •ํ™•๋„: {model_info['accuracy2']}%)")
1210
-
1211
- # ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง
1212
- item_df = raw_df.query("item == @selected_item").copy()
1213
- if item_df.empty:
1214
- st.error("์„ ํƒํ•œ ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ ์—†์Œ")
1215
- st.stop()
1216
-
1217
- # ๋ฐ์ดํ„ฐ ์ˆ˜ ๊ฒ€์‚ฌ
1218
- if len(item_df) < 2:
1219
- st.warning(f"์„ ํƒํ•œ ํ’ˆ๋ชฉ '{selected_item}' ๋ฐ์ดํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ์ ์Šต๋‹ˆ๋‹ค (๋ฐ์ดํ„ฐ ์ˆ˜: {len(item_df)}). ์˜ˆ์ธก์ด ๋ถ€์ •ํ™•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
1220
- else:
1221
- st.success(f"์„ ํƒํ•œ ํ’ˆ๋ชฉ '{selected_item}'์— ๋Œ€ํ•ด {len(item_df)}๊ฐœ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.")
1222
-
1223
- # -------------------------------------------------
1224
- # MACRO FORECAST 1996โ€‘2030 ------------------------
1225
- # -------------------------------------------------
1226
- # -------------------------------------------------
1227
- # MACRO FORECAST 1996โ€‘2030 ------------------------
1228
- # -------------------------------------------------
1229
- st.header(f"๐Ÿ“ˆ {selected_item} ๊ฐ€๊ฒฉ ์˜ˆ์ธก ๋Œ€์‹œ๋ณด๋“œ")
1230
-
1231
- # ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง ๋กœ์ง
1232
- try:
1233
- macro_start_dt = pd.Timestamp("1996-01-01")
1234
- # ๋ฐ์ดํ„ฐ์˜ ์‹œ์ž‘์ผ์ด 1996๋…„ ์ดํ›„์ธ์ง€ ํ™•์ธ
1235
- if item_df["date"].min() > macro_start_dt:
1236
- macro_start_dt = item_df["date"].min()
1237
-
1238
- macro_df = item_df[item_df["date"] >= macro_start_dt].copy()
1239
- except Exception as e:
1240
- st.error(f"๋‚ ์งœ ํ•„ํ„ฐ๋ง ์˜ค๋ฅ˜: {str(e)}")
1241
- macro_df = item_df.copy() # ํ•„ํ„ฐ๋ง ์—†์ด ์ „์ฒด ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ
1242
-
1243
- # Add diagnostic info
1244
- with st.expander("๋ฐ์ดํ„ฐ ์ง„๋‹จ"):
1245
- st.write(f"- ์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜: {len(item_df)}")
1246
- st.write(f"- ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ˆ˜: {len(macro_df)}")
1247
- if len(macro_df) > 0:
1248
- st.write(f"- ๊ธฐ๊ฐ„: {macro_df['date'].min().strftime('%Y-%m-%d')} ~ {macro_df['date'].max().strftime('%Y-%m-%d')}")
1249
- st.dataframe(macro_df.head())
1250
- else:
1251
- st.write("๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
1252
-
1253
- if len(macro_df) < 2:
1254
- st.warning(f"{selected_item}์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ „์ฒด ๊ธฐ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.")
1255
- fig = go.Figure()
1256
- fig.add_trace(go.Scatter(x=item_df["date"], y=item_df["price"], mode="lines", name="์‹ค์ œ ๊ฐ€๊ฒฉ"))
1257
- fig.update_layout(title=f"{selected_item} ๊ณผ๊ฑฐ ๊ฐ€๊ฒฉ")
1258
- st.plotly_chart(fig, use_container_width=True)
1259
- else:
1260
- try:
1261
- # ๋ฐ์ดํ„ฐ ์ถฉ๋ถ„ํ•œ ๊ฒฝ์šฐ ํ’ˆ๋ชฉ๋ณ„ ์ตœ์  ๋ชจ๋ธ ์‚ฌ์šฉ
1262
- use_ensemble = st.checkbox("์•™์ƒ๋ธ” ๋ชจ๋ธ ์‚ฌ์šฉ (1์œ„ + 2์œ„ ๋ชจ๋ธ ๊ฒฐํ•ฉ)", value=False)
1263
-
1264
- with st.spinner("์žฅ๊ธฐ ์˜ˆ์ธก ๋ชจ๋ธ ์ƒ์„ฑ ์ค‘..."):
1265
- if use_ensemble:
1266
- fc_macro = fit_ensemble_model(macro_df, selected_item, MACRO_END)
1267
- else:
1268
- fc_macro = fit_optimal_model(macro_df, selected_item, MACRO_END)
1269
-
1270
- if fc_macro is not None:
1271
- # ์‹ค์ œ ๋ฐ์ดํ„ฐ์™€ ์˜ˆ์ธก ๋ฐ์ดํ„ฐ ๊ตฌ๋ถ„
1272
- cutoff_date = pd.Timestamp("2025-01-01")
1273
-
1274
- # ํ”Œ๋กฏ ์ƒ์„ฑ
1275
- fig = go.Figure()
1276
-
1277
- # ์‹ค์ œ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (1996-2024)
1278
- historical_data = macro_df[macro_df["date"] < cutoff_date].copy()
1279
- if not historical_data.empty:
1280
- fig.add_trace(go.Scatter(
1281
- x=historical_data["date"],
1282
- y=historical_data["price"],
1283
- mode="lines",
1284
- name="์‹ค์ œ ๊ฐ€๊ฒฉ (1996-2024)",
1285
- line=dict(color="blue", width=2)
1286
- ))
1287
-
1288
- # ์˜ˆ์ธก ๊ธฐ๊ฐ„ ์ž๋ฅด๊ธฐ
1289
- forecast_data = fc_macro[fc_macro["ds"] >= cutoff_date].copy()
1290
-
1291
- # 2025-2030 ์˜ˆ์ธก ๋ฐ์ดํ„ฐ
1292
- if not forecast_data.empty:
1293
- fig.add_trace(go.Scatter(
1294
- x=forecast_data["ds"],
1295
- y=forecast_data["yhat"],
1296
- mode="lines",
1297
- name="์˜ˆ์ธก ๊ฐ€๊ฒฉ (2025-2030)",
1298
- line=dict(color="red", width=2, dash="dash")
1299
- ))
1300
-
1301
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”๊ฐ€
1302
- fig.add_trace(go.Scatter(
1303
- x=forecast_data["ds"],
1304
- y=forecast_data["yhat_upper"],
1305
- mode="lines",
1306
- line=dict(width=0),
1307
- showlegend=False
1308
- ))
1309
- fig.add_trace(go.Scatter(
1310
- x=forecast_data["ds"],
1311
- y=forecast_data["yhat_lower"],
1312
- mode="lines",
1313
- line=dict(width=0),
1314
- fill="tonexty",
1315
- fillcolor="rgba(255, 0, 0, 0.1)",
1316
- name="95% ์‹ ๋ขฐ ๊ตฌ๊ฐ„"
1317
- ))
1318
-
1319
- # ์Œ์ˆ˜ ์˜ˆ์ธก๊ฐ’ ์ œ๊ฑฐ
1320
- fig.update_yaxes(range=[0, None])
1321
-
1322
- # ๋ ˆ์ด์•„์›ƒ ์„ค์ •
1323
- fig.update_layout(
1324
- title=f"{selected_item} ์žฅ๊ธฐ ๊ฐ€๊ฒฉ ์˜ˆ์ธก (1996-2030)",
1325
- xaxis_title="์—ฐ๋„",
1326
- yaxis_title="๊ฐ€๊ฒฉ (์›)",
1327
- legend=dict(
1328
- orientation="h",
1329
- yanchor="bottom",
1330
- y=1.02,
1331
- xanchor="right",
1332
- x=1
1333
- )
1334
- )
1335
-
1336
- # ์ฐจํŠธ ํ‘œ์‹œ
1337
- st.plotly_chart(fig, use_container_width=True)
1338
-
1339
- # ์—ฐ๋„๋ณ„ ์˜ˆ์ธก๊ฐ€ ํ‘œ์‹œ
1340
- try:
1341
- latest_price = macro_df.iloc[-1]["price"]
1342
-
1343
- # ์—ฐ๋„๋ณ„ ์˜ˆ์ธก๊ฐ€ ๊ณ„์‚ฐ์„ ์œ„ํ•œ ํ•จ์ˆ˜
1344
- def get_yearly_prediction(year_end):
1345
- target_date = pd.Timestamp(f"{year_end}-12-31")
1346
- # ๋‚ ์งœ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๋‚ ์งœ์˜ ์˜ˆ์ธก๊ฐ’ ์ฐพ๊ธฐ
1347
- date_diffs = abs(fc_macro["ds"] - target_date)
1348
- closest_idx = date_diffs.idxmin()
1349
- pred_value = fc_macro.loc[closest_idx, "yhat"]
1350
- pct_change = (pred_value - latest_price) / latest_price * 100
1351
- return pred_value, pct_change
1352
-
1353
- # ์—ฐ๋„๋ณ„ ์˜ˆ์ธก๊ฐ€ ํ‘œ์‹œ
1354
- col1, col2, col3 = st.columns(3)
1355
-
1356
- # 2025๋…„ ์˜ˆ์ธก๊ฐ€
1357
- pred_2025, pct_2025 = get_yearly_prediction(2025)
1358
- col1.metric("2025๋…„ ์˜ˆ์ธก๊ฐ€", format_currency(pred_2025), f"{pct_2025:+.1f}%")
1359
-
1360
- # 2027๋…„ ์˜ˆ์ธก๊ฐ€
1361
- pred_2027, pct_2027 = get_yearly_prediction(2027)
1362
- col2.metric("2027๋…„ ์˜ˆ์ธก๊ฐ€", format_currency(pred_2027), f"{pct_2027:+.1f}%")
1363
-
1364
- # 2030๋…„ ์˜ˆ์ธก๊ฐ€
1365
- pred_2030, pct_2030 = get_yearly_prediction(2030)
1366
- col3.metric("2030๋…„ ์˜ˆ์ธก๊ฐ€", format_currency(pred_2030), f"{pct_2030:+.1f}%")
1367
-
1368
- # ์ถ”๊ฐ€ ์—ฐ๋„ ์˜ˆ์ธก๊ฐ€ (ํ™•์žฅ ๊ฐ€๋Šฅ)
1369
- with st.expander("๋” ๋งŽ์€ ์—ฐ๋„๋ณ„ ์˜ˆ์ธก๊ฐ€ ๋ณด๊ธฐ"):
1370
- col4, col5, col6 = st.columns(3)
1371
-
1372
- # 2026๋…„ ์˜ˆ์ธก๊ฐ€
1373
- pred_2026, pct_2026 = get_yearly_prediction(2026)
1374
- col4.metric("2026๋…„ ์˜ˆ์ธก๊ฐ€", format_currency(pred_2026), f"{pct_2026:+.1f}%")
1375
-
1376
- # 2028๋…„ ์˜ˆ์ธก๊ฐ€
1377
- pred_2028, pct_2028 = get_yearly_prediction(2028)
1378
- col5.metric("2028๋…„ ์˜ˆ์ธก๊ฐ€", format_currency(pred_2028), f"{pct_2028:+.1f}%")
1379
-
1380
- # 2029๋…„ ์˜ˆ์ธก๊ฐ€
1381
- pred_2029, pct_2029 = get_yearly_prediction(2029)
1382
- col6.metric("2029๋…„ ์˜ˆ์ธก๊ฐ€", format_currency(pred_2029), f"{pct_2029:+.1f}%")
1383
-
1384
- except Exception as e:
1385
- st.error(f"์˜ˆ์ธก๊ฐ€ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: {str(e)}")
1386
- else:
1387
- st.warning("์˜ˆ์ธก ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
1388
- fig = go.Figure()
1389
- fig.add_trace(go.Scatter(x=macro_df["date"], y=macro_df["price"], mode="lines", name="์‹ค์ œ ๊ฐ€๊ฒฉ"))
1390
- fig.update_layout(title=f"{selected_item} ๊ณผ๊ฑฐ ๊ฐ€๊ฒฉ")
1391
- st.plotly_chart(fig, use_container_width=True)
1392
- except Exception as e:
1393
- st.error(f"์žฅ๊ธฐ ์˜ˆ์ธก ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
1394
  import traceback
1395
  st.code(traceback.format_exc())
1396
- fig = go.Figure()
1397
- fig.add_trace(go.Scatter(x=macro_df["date"], y=macro_df["price"], mode="lines", name="์‹ค์ œ ๊ฐ€๊ฒฉ"))
1398
- fig.update_layout(title=f"{selected_item} ๊ณผ๊ฑฐ ๊ฐ€๊ฒฉ")
1399
- st.plotly_chart(fig, use_container_width=True)
1400
-
1401
- # -------------------------------------------------
1402
- # MICRO FORECAST 2024โ€‘2026 ------------------------
1403
- # -------------------------------------------------
1404
- # -------------------------------------------------
1405
- # MICRO FORECAST 2024โ€‘2026 ------------------------
1406
- # -------------------------------------------------
1407
- st.subheader("๐Ÿ”Ž 2024โ€“2026 ๋‹จ๊ธฐ ์˜ˆ์ธก (์›”๋ณ„)")
1408
-
1409
- # ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง - ์ตœ๊ทผ 3๋…„ ๋ฐ์ดํ„ฐ ํ™œ์šฉ
1410
- try:
1411
- three_years_ago = pd.Timestamp("2021-01-01")
1412
- if item_df["date"].min() > three_years_ago:
1413
- three_years_ago = item_df["date"].min()
1414
-
1415
- micro_df = item_df[item_df["date"] >= three_years_ago].copy()
1416
- except Exception as e:
1417
- st.error(f"๋‹จ๊ธฐ ์˜ˆ์ธก ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง ์˜ค๋ฅ˜: {str(e)}")
1418
- # ์ตœ๊ทผ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ
1419
- micro_df = item_df.sort_values("date").tail(24).copy()
1420
-
1421
- if len(micro_df) < 2:
1422
- st.warning(f"์ตœ๊ทผ ๋ฐ์ดํ„ฐ๊ฐ€ ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
1423
- fig = go.Figure()
1424
- fig.add_trace(go.Scatter(x=item_df["date"], y=item_df["price"], mode="lines", name="์‹ค์ œ ๊ฐ€๊ฒฉ"))
1425
- fig.update_layout(title=f"{selected_item} ์ตœ๊ทผ ๊ฐ€๊ฒฉ")
1426
- st.plotly_chart(fig, use_container_width=True)
1427
- else:
1428
- try:
1429
- with st.spinner("๋‹จ๊ธฐ ์˜ˆ์ธก ๋ชจ๋ธ ์ƒ์„ฑ ์ค‘..."):
1430
- if use_ensemble:
1431
- fc_micro = fit_ensemble_model(micro_df, selected_item, MICRO_END)
1432
- else:
1433
- fc_micro = fit_optimal_model(micro_df, selected_item, MICRO_END)
1434
-
1435
- if fc_micro is not None:
1436
- # 2024-01-01๋ถ€ํ„ฐ 2026-12-31๊นŒ์ง€ ํ•„ํ„ฐ๋ง
1437
- start_date = pd.Timestamp("2024-01-01")
1438
- end_date = pd.Timestamp("2026-12-31")
1439
-
1440
- # ์›”๋ณ„ ๋ฐ์ดํ„ฐ ์ค€๋น„
1441
- monthly_historical = micro_df.copy()
1442
- monthly_historical["year_month"] = monthly_historical["date"].dt.strftime("%Y-%m")
1443
- monthly_historical = monthly_historical.groupby("year_month").agg({
1444
- "date": "first",
1445
- "price": "mean"
1446
- }).reset_index(drop=True)
1447
-
1448
- monthly_historical = monthly_historical[
1449
- (monthly_historical["date"] >= start_date) &
1450
- (monthly_historical["date"] <= end_date)
1451
- ]
1452
-
1453
- monthly_forecast = fc_micro[
1454
- (fc_micro["ds"] >= start_date) &
1455
- (fc_micro["ds"] <= end_date)
1456
- ].copy()
1457
-
1458
- # ์›”๋ณ„ ์ฐจํŠธ ์ƒ์„ฑ
1459
- fig = go.Figure()
1460
-
1461
- # 2024๋…„ ์‹ค์ œ ๋ฐ์ดํ„ฐ
1462
- actual_2024 = monthly_historical[
1463
- (monthly_historical["date"] >= pd.Timestamp("2024-01-01")) &
1464
- (monthly_historical["date"] <= pd.Timestamp("2024-12-31"))
1465
- ]
1466
-
1467
- if not actual_2024.empty:
1468
- fig.add_trace(go.Scatter(
1469
- x=actual_2024["date"],
1470
- y=actual_2024["price"],
1471
- mode="lines+markers",
1472
- name="2024 ์‹ค์ œ ๊ฐ€๊ฒฉ",
1473
- line=dict(color="blue", width=2),
1474
- marker=dict(size=8)
1475
- ))
1476
-
1477
- # 2024๋…„ ์ดํ›„ ์˜ˆ์ธก ๋ฐ์ดํ„ฐ
1478
- cutoff = pd.Timestamp("2024-12-31")
1479
- future_data = monthly_forecast[monthly_forecast["ds"] > cutoff]
1480
-
1481
- if not future_data.empty:
1482
- fig.add_trace(go.Scatter(
1483
- x=future_data["ds"],
1484
- y=future_data["yhat"],
1485
- mode="lines+markers",
1486
- name="2025-2026 ์˜ˆ์ธก ๊ฐ€๊ฒฉ",
1487
- line=dict(color="red", width=2, dash="dash"),
1488
- marker=dict(size=8)
1489
- ))
1490
-
1491
- # ์‹ ๋ขฐ ๊ตฌ๊ฐ„ ์ถ”๊ฐ€
1492
- fig.add_trace(go.Scatter(
1493
- x=future_data["ds"],
1494
- y=future_data["yhat_upper"],
1495
- mode="lines",
1496
- line=dict(width=0),
1497
- showlegend=False
1498
- ))
1499
- fig.add_trace(go.Scatter(
1500
- x=future_data["ds"],
1501
- y=future_data["yhat_lower"],
1502
- mode="lines",
1503
- line=dict(width=0),
1504
- fill="tonexty",
1505
- fillcolor="rgba(255, 0, 0, 0.1)",
1506
- name="95% ์‹ ๋ขฐ ๊ตฌ๊ฐ„"
1507
- ))
1508
-
1509
- # ์Œ์ˆ˜ ์˜ˆ์ธก๊ฐ’ ์ œ๊ฑฐ
1510
- fig.update_yaxes(range=[0, None])
1511
-
1512
- # ๋ ˆ์ด์•„์›ƒ ์„ค์ •
1513
- fig.update_layout(
1514
- title=f"{selected_item} ์›”๋ณ„ ๋‹จ๊ธฐ ์˜ˆ์ธก (2024-2026)",
1515
- xaxis_title="์›”",
1516
- yaxis_title="๊ฐ€๊ฒฉ (์›)",
1517
- xaxis=dict(
1518
- tickformat="%Y-%m",
1519
- dtick="M3", # 3๊ฐœ์›” ๊ฐ„๊ฒฉ
1520
- tickangle=45
1521
- ),
1522
- legend=dict(
1523
- orientation="h",
1524
- yanchor="bottom",
1525
- y=1.02,
1526
- xanchor="right",
1527
- x=1
1528
- )
1529
- )
1530
-
1531
- # ์ฐจํŠธ ํ‘œ์‹œ
1532
- st.plotly_chart(fig, use_container_width=True)
1533
-
1534
- # ์›”๋ณ„ ์˜ˆ์ธก ๊ฐ€๊ฒฉ ํ‘œ์‹œ (2025-2026)
1535
- with st.expander("์›”๋ณ„ ์˜ˆ์ธก ๊ฐ€๊ฒฉ ์ƒ์„ธ๋ณด๊ธฐ"):
1536
- monthly_detail = monthly_forecast[monthly_forecast["ds"] > cutoff].copy()
1537
- monthly_detail["๋‚ ์งœ"] = monthly_detail["ds"].dt.strftime("%Y๋…„ %m์›”")
1538
- monthly_detail["์˜ˆ์ธก๊ฐ€๊ฒฉ"] = monthly_detail["yhat"].apply(format_currency)
1539
- monthly_detail["ํ•˜ํ•œ๊ฐ’"] = monthly_detail["yhat_lower"].apply(format_currency)
1540
- monthly_detail["์ƒํ•œ๊ฐ’"] = monthly_detail["yhat_upper"].apply(format_currency)
1541
-
1542
- st.dataframe(
1543
- monthly_detail[["๋‚ ์งœ", "์˜ˆ์ธก๊ฐ€๊ฒฉ", "ํ•˜ํ•œ๊ฐ’", "์ƒํ•œ๊ฐ’"]],
1544
- hide_index=True
1545
- )
1546
-
1547
- # ์›”๋ณ„/์—ฐ๋„๋ณ„ ์˜ˆ์ธก๊ฐ€ ํ‘œ์‹œ ํ•จ์ˆ˜
1548
- def get_monthly_prediction(year, month):
1549
- target_date = pd.Timestamp(f"{year}-{month:02d}-01")
1550
- # ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๋‚ ์งœ์˜ ์˜ˆ์ธก๊ฐ’ ์ฐพ๊ธฐ
1551
- date_diffs = abs(monthly_forecast["ds"] - target_date)
1552
- closest_idx = date_diffs.idxmin()
1553
-
1554
- if closest_idx in monthly_forecast.index:
1555
- pred_value = monthly_forecast.loc[closest_idx, "yhat"]
1556
-
1557
- # ํ˜„์žฌ ๊ฐ€๊ฒฉ ๊ธฐ์ค€ ๋ณ€ํ™”์œจ ๊ณ„์‚ฐ
1558
- latest_price = monthly_historical.iloc[-1]["price"] if not monthly_historical.empty else micro_df.iloc[-1]["price"]
1559
- pct_change = (pred_value - latest_price) / latest_price * 100
1560
-
1561
- return pred_value, pct_change
1562
- else:
1563
- return None, None
1564
-
1565
- # 2025๋…„๊ณผ 2026๋…„์˜ ์ฃผ์š” ์›”๋ณ„ ์˜ˆ์ธก๊ฐ€
1566
- st.subheader("์ฃผ์š” ์›”๋ณ„ ์˜ˆ์ธก๊ฐ€")
1567
-
1568
- col1, col2, col3 = st.columns(3)
1569
-
1570
- # 2025๋…„ 6์›” ์˜ˆ์ธก๊ฐ€
1571
- pred_2025_06, pct_2025_06 = get_monthly_prediction(2025, 6)
1572
- if pred_2025_06 is not None:
1573
- col1.metric("2025๋…„ 6์›”", format_currency(pred_2025_06), f"{pct_2025_06:+.1f}%")
1574
- else:
1575
- col1.metric("2025๋…„ 6์›”", "๋ฐ์ดํ„ฐ ์—†์Œ", "0%")
1576
-
1577
- # 2025๋…„ 12์›” ์˜ˆ์ธก๊ฐ€
1578
- pred_2025_12, pct_2025_12 = get_monthly_prediction(2025, 12)
1579
- if pred_2025_12 is not None:
1580
- col2.metric("2025๋…„ 12์›”", format_currency(pred_2025_12), f"{pct_2025_12:+.1f}%")
1581
- else:
1582
- col2.metric("2025๋…„ 12์›”", "๋ฐ์ดํ„ฐ ์—†์Œ", "0%")
1583
-
1584
- # 2026๋…„ 12์›” ์˜ˆ์ธก๊ฐ€
1585
- pred_2026_12, pct_2026_12 = get_monthly_prediction(2026, 12)
1586
- if pred_2026_12 is not None:
1587
- col3.metric("2026๋…„ 12์›”", format_currency(pred_2026_12), f"{pct_2026_12:+.1f}%")
1588
- else:
1589
- col3.metric("2026๋…„ 12์›”", "๋ฐ์ดํ„ฐ ์—†์Œ", "0%")
1590
-
1591
- # ๋†์‚ฐ๋ฌผ ๊ณ„์ ˆ์„ฑ์— ๋งž๋Š” ์ถ”๊ฐ€ ์›”๋ณ„ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ
1592
- with st.expander("๋” ๋งŽ์€ ์›”๋ณ„ ์˜ˆ์ธก๊ฐ€ ๋ณด๊ธฐ"):
1593
- # ๋ถ„๊ธฐ๋ณ„๋กœ ๋‚˜๋ˆ ์„œ ํ‘œ์‹œ
1594
- for year in [2025, 2026]:
1595
- st.write(f"### {year}๋…„ ๋ถ„๊ธฐ๋ณ„ ์˜ˆ์ธก๊ฐ€")
1596
- q1, q2, q3, q4 = st.columns(4)
1597
-
1598
- # 1๋ถ„๊ธฐ (3์›”)
1599
- pred_q1, pct_q1 = get_monthly_prediction(year, 3)
1600
- if pred_q1 is not None:
1601
- q1.metric(f"{year}๋…„ 3์›”", format_currency(pred_q1), f"{pct_q1:+.1f}%")
1602
- else:
1603
- q1.metric(f"{year}๋…„ 3์›”", "๋ฐ์ดํ„ฐ ์—†์Œ", "0%")
1604
-
1605
- # 2๋ถ„๊ธฐ (6์›”)
1606
- pred_q2, pct_q2 = get_monthly_prediction(year, 6)
1607
- if pred_q2 is not None:
1608
- q2.metric(f"{year}๋…„ 6์›”", format_currency(pred_q2), f"{pct_q2:+.1f}%")
1609
- else:
1610
- q2.metric(f"{year}๋…„ 6์›”", "๋ฐ์ดํ„ฐ ์—†์Œ", "0%")
1611
-
1612
- # 3๋ถ„๊ธฐ (9์›”)
1613
- pred_q3, pct_q3 = get_monthly_prediction(year, 9)
1614
- if pred_q3 is not None:
1615
- q3.metric(f"{year}๋…„ 9์›”", format_currency(pred_q3), f"{pct_q3:+.1f}%")
1616
- else:
1617
- q3.metric(f"{year}๋…„ 9์›”", "๋ฐ์ดํ„ฐ ์—†์Œ", "0%")
1618
-
1619
- # 4๋ถ„๊ธฐ (12์›”)
1620
- pred_q4, pct_q4 = get_monthly_prediction(year, 12)
1621
- if pred_q4 is not None:
1622
- q4.metric(f"{year}๋…„ 12์›”", format_currency(pred_q4), f"{pct_q4:+.1f}%")
1623
- else:
1624
- q4.metric(f"{year}๋…„ 12์›”", "๋ฐ์ดํ„ฐ ์—†์Œ", "0%")
1625
-
1626
- else:
1627
- st.warning("๋‹จ๊ธฐ ์˜ˆ์ธก ๋ชจ๋ธ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
1628
- except Exception as e:
1629
- st.error(f"๋‹จ๊ธฐ ์˜ˆ์ธก ์˜ค๋ฅ˜: {str(e)}")
1630
- st.code(traceback.format_exc())
1631
-
1632
- # -------------------------------------------------
1633
- # SEASONALITY & PATTERN ---------------------------
1634
- # -------------------------------------------------
1635
- if 'fc_micro' in locals() and fc_micro is not None:
1636
- with st.expander("๐Ÿ“† ์‹œ์ฆˆ๋„๋ฆฌํ‹ฐ & ํŒจํ„ด ์„ค๋ช…"):
1637
- try:
1638
- # ์›”๋ณ„ ๊ณ„์ ˆ์„ฑ ๋ถ„์„
1639
- if "yearly" in fc_micro.columns and fc_micro["yearly"].sum() != 0:
1640
- month_season = fc_micro.copy()
1641
- month_season["month"] = month_season["ds"].dt.month
1642
- month_seasonality = month_season.groupby("month")["yearly"].mean()
1643
-
1644
- # ์›” ์ด๋ฆ„ ์„ค์ •
1645
- month_names = ["1์›”", "2์›”", "3์›”", "4์›”", "5์›”", "6์›”", "7์›”", "8์›”", "9์›”", "10์›”", "11์›”", "12์›”"]
1646
-
1647
- # ๊ณ„์ ˆ์„ฑ ์ฐจํŠธ ๊ทธ๋ฆฌ๊ธฐ
1648
- fig = go.Figure()
1649
- fig.add_trace(go.Bar(
1650
- x=month_names,
1651
- y=month_seasonality.values,
1652
- marker_color=['blue' if x >= 0 else 'red' for x in month_seasonality.values]
1653
- ))
1654
-
1655
- fig.update_layout(
1656
- title=f"{selected_item} ์›”๋ณ„ ๊ณ„์ ˆ์„ฑ ํŒจํ„ด",
1657
- xaxis_title="์›”",
1658
- yaxis_title="์ƒ๋Œ€์  ๊ฐ€๊ฒฉ ๋ณ€๋™",
1659
- )
1660
-
1661
- st.plotly_chart(fig, use_container_width=True)
1662
-
1663
- # ํ”ผํฌ์™€ ์ €์  ๊ณ„์‚ฐ
1664
- peak_month = month_seasonality.idxmax()
1665
- low_month = month_seasonality.idxmin()
1666
- seasonality_range = month_seasonality.max() - month_seasonality.min()
1667
-
1668
- st.markdown(
1669
- f"**์—ฐ๊ฐ„ ํ”ผํฌ ์›”:** {month_names[peak_month-1]} \n"
1670
- f"**์—ฐ๊ฐ„ ์ €์  ์›”:** {month_names[low_month-1]} \n"
1671
- f"**์—ฐ๊ฐ„ ๋ณ€๋™ํญ:** {seasonality_range:.1f}")
1672
-
1673
- # ๊ณ„์ ˆ์„ฑ์ด ๋†’์€ ํ’ˆ๋ชฉ์ธ์ง€ ์„ค๋ช…
1674
- if abs(seasonality_range) > 30:
1675
- st.info(f"{selected_item}์€(๋Š”) ๊ณ„์ ˆ์„ฑ์ด ๋งค์šฐ ๊ฐ•ํ•œ ํ’ˆ๋ชฉ์ž…๋‹ˆ๋‹ค. ํŠน์ • ๋‹ฌ์— ๊ฐ€๊ฒฉ์ด ํฌ๊ฒŒ ๋ณ€๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
1676
- elif abs(seasonality_range) > 10:
1677
- st.info(f"{selected_item}์€(๋Š”) ๊ณ„์ ˆ์„ฑ์ด ์ค‘๊ฐ„ ์ •๋„์ธ ํ’ˆ๋ชฉ์ž…๋‹ˆ๋‹ค.")
1678
- else:
1679
- st.info(f"{selected_item}์€(๋Š”) ๊ณ„์ ˆ์„ฑ์ด ์•ฝํ•œ ํ’ˆ๋ชฉ์ž…๋‹ˆ๋‹ค. ์—ฐ์ค‘ ๊ฐ€๊ฒฉ์ด ๋น„๊ต์  ์•ˆ์ •์ ์ž…๋‹ˆ๋‹ค.")
1680
- except Exception as e:
1681
- st.error(f"๊ณ„์ ˆ์„ฑ ๋ถ„์„ ์˜ค๋ฅ˜: {str(e)}")
1682
- st.info("์ด ํ’ˆ๋ชฉ์— ๋Œ€ํ•œ ๊ณ„์ ˆ์„ฑ ํŒจํ„ด์„ ๋ถ„์„ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
1683
 
1684
- # -------------------------------------------------
1685
- # FOOTER ------------------------------------------
1686
- # -------------------------------------------------
1687
- st.markdown("---")
1688
- st.caption("ยฉ 2025 ํ’ˆ๋ชฉ๋ณ„ ๊ฐ€๊ฒฉ ์˜ˆ์ธก ์‹œ์Šคํ…œ | ๋ฐ์ดํ„ฐ ๋ถ„์„ ์ž๋™ํ™”")
 
1
+ import os
2
+ import sys
3
  import streamlit as st
4
+ from tempfile import NamedTemporaryFile
 
 
 
 
 
 
 
 
 
5
 
6
+ def main():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  try:
8
+ # Get the code from secrets
9
+ code = os.environ.get("MAIN_CODE")
 
 
 
 
 
 
 
 
10
 
11
+ if not code:
12
+ st.error("โš ๏ธ The application code wasn't found in secrets. Please add the MAIN_CODE secret.")
13
+ return
 
 
 
14
 
15
+ # Create a temporary Python file
16
+ with NamedTemporaryFile(suffix='.py', delete=False, mode='w') as tmp:
17
+ tmp.write(code)
18
+ tmp_path = tmp.name
 
 
 
 
 
 
 
19
 
20
+ # Execute the code
21
+ exec(compile(code, tmp_path, 'exec'), globals())
22
 
23
+ # Clean up the temporary file
24
  try:
25
+ os.unlink(tmp_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  except:
27
  pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  except Exception as e:
30
+ st.error(f"โš ๏ธ Error loading or executing the application: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  import traceback
32
  st.code(traceback.format_exc())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ if __name__ == "__main__":
35
+ main()