Spaces:
Paused
Paused
| #!/usr/bin/env python | |
| # -*- coding: utf-8 -*- | |
| # ============================================================ | |
| # ํธ์์ ์์์์ธก & ๋ฐ์ฃผ ์ถ์ฒ โ Pro Suite (ํจ์น ๋ฒ์ , ๋ฉํฐ CSV + ์๋ณ ๊ทธ๋ํ) | |
| # - โ ์ฌ๋ฌ CSV ์ ๋ก๋/์ ํ โ ์๋ ๊ฒฐํฉ(์ต์ : source ์ด ์ถ๊ฐ) | |
| # - โก ์ปฌ๋ผ ๋งคํ: "์ปฌ๋ผ๋ช "์ด ์๋๋ผ "์์ ๊ฐ" ๊ธฐ๋ฐ ์ ํ | |
| # - โข ์์ธกยท๋ฐ์ฃผ: ์ฌ๊ณ ์ปฌ๋ผ ์๋ ์ธ์ โ ์์ธก ๊ธฐ๊ฐ/๋ฐ์ฃผ๋ ์๋ ๊ณ์ฐ | |
| # ยท ๋ฆฌ๋ํ์ / ์๋น์ค๋ ๋ฒจ / ์์ ์ฌ๊ณ / MOQ / ํฉ๋จ์ ์ ๋ ฅ ์ ๊ฑฐ | |
| # - โฃ ๋ถ์(๊ทธ๋ํ): | |
| # ยท ์ฐ์ฐ: ์๋ณ ๊ฐ์๋ โ ์ฐ์ฐ ํ๋งค๋ (์ฐ์ ๋ + ํ๊ท์ + ์ผ๋ณ ์ ํ ๊ทธ๋ํ) | |
| # ยท ๊ตฐ๊ณ ๊ตฌ๋ง: ์๋ณ ๊ธฐ์จ โ ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋ (์ฐ์ ๋ + ํ๊ท์ + ์ผ๋ณ ์ ํ ๊ทธ๋ํ) | |
| # ยท ์ ์ฒด: ์ฐ์ฐยท๊ตฐ๊ณ ๊ตฌ๋ง ์ ์ธ ์ ์ฒด ์ํ ์ผ๋ณ ํ๋งค๋ ์ ํ ๊ทธ๋ํ | |
| # - ์ฌ์ด๋๋ฐ: ์คํ ํ์ผ ํ์ + ์บ์ ์ด๊ธฐํ | |
| # ============================================================ | |
| import os, io, pickle, time, subprocess, sys | |
| from datetime import timedelta | |
| from pathlib import Path | |
| import pandas as pd | |
| import numpy as np | |
| import streamlit as st | |
| import altair as alt | |
| from utils_io import read_csv_flexible, save_utf8sig, ensure_dirs, auto_map_columns | |
| from preprocess import make_matrix | |
| from train_core import train_and_score, save_artifacts | |
| # Altair ๋์ฉ๋ ๋ ๋๋ง ์์ ์ฅ์น (ํ ์ ์ ํ ํด์ ) | |
| alt.data_transformers.disable_max_rows() | |
| # ------------------------------------------------------------ | |
| # ํ์ด์ง/์ฌ์ด๋๋ฐ | |
| # ------------------------------------------------------------ | |
| st.set_page_config(page_title="ํธ์์ ์์์์ธก & ๋ฐ์ฃผ ์ถ์ฒ โ Pro Suite (ํจ์น)", layout="wide") | |
| # __file__ ์ด ์๋ Colab ๊ฐ์ ํ๊ฒฝ ๋ฐฉ์ด์ฉ | |
| try: | |
| script_name = Path(__file__).resolve().name | |
| except NameError: | |
| script_name = "app_streamlit_pro.py" | |
| st.sidebar.write("๐งญ ์คํ ํ์ผ:", script_name) | |
| if st.sidebar.button("์บ์ ์ด๊ธฐํ ํ ๋ค์ ์คํ"): | |
| try: | |
| st.cache_data.clear() | |
| except Exception: | |
| pass | |
| try: | |
| st.cache_resource.clear() | |
| except Exception: | |
| pass | |
| st.experimental_rerun() | |
| # ------------------------------------------------------------ | |
| # ๊ธฐ๋ณธ ํ๊ฒฝ/๊ฒฝ๋ก ์ค์ | |
| # ------------------------------------------------------------ | |
| PROJ = os.getcwd() # ํ์ฌ ์์ ๋๋ ํ ๋ฆฌ(์ฑ ๋ฃจํธ) | |
| DATA_DIR = os.path.join(PROJ, "data") # CSV ๋ฐ์ดํฐ ํด๋ | |
| ARTI_DIR = os.path.join(PROJ, "artifacts") # ํ์ต ์ค๊ฐ์ฐ์ถ๋ฌผ(๋ก๊ทธ/์ฑ๋ฅ ๋ฑ) ๋ณด๊ด | |
| MODELS_DIR = os.path.join(PROJ, "models") # ํ์ต๋ ๋ชจ๋ธ pkl ๋ณด๊ด | |
| ensure_dirs(DATA_DIR, ARTI_DIR, MODELS_DIR) # ํด๋ ์์ผ๋ฉด ์์ฑ | |
| # ------------------------------------------------------------ | |
| # ์ ํธ: data ํด๋์ CSV ํ์ผ ๋ฆฌ์คํธ ์บ์ | |
| # ------------------------------------------------------------ | |
| def list_data_files(): | |
| try: | |
| return [f for f in os.listdir(DATA_DIR) if f.lower().endswith(".csv")] | |
| except FileNotFoundError: | |
| return [] | |
| # ------------------------------------------------------------ | |
| # ํผ๋ธ๋ฆญ URL: cloudflared ์์ ํจ์ | |
| # ------------------------------------------------------------ | |
| def start_cloudflared(port=8501): | |
| try: | |
| proc = subprocess.Popen( | |
| ["cloudflared", "tunnel", "--url", f"http://localhost:{port}"], | |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True | |
| ) | |
| st.session_state["_cfd_proc"] = proc # ์ข ๋ฃ์ฉ ํธ๋ค ์ ์ฅ | |
| with st.expander("cloudflared logs"): | |
| for _ in range(120): # ์ต์ด 120๋ผ์ธ ์ ๋๋ง ์ฝ์ด ํ์ | |
| line = proc.stdout.readline() | |
| if not line: | |
| break | |
| st.text(line.strip()) | |
| if "trycloudflare.com" in line: | |
| st.success(line.strip()) # ํผ๋ธ๋ฆญ URL ํฌํจ ๋ก๊ทธ | |
| break | |
| except FileNotFoundError: | |
| st.error("cloudflared ๋ฐ์ด๋๋ฆฌ๊ฐ ์์ต๋๋ค. `pip install cloudflared` ๋๋ ๋ฐ์ด๋๋ฆฌ ์ค์น ํ ๋ค์ ์๋ํ์ธ์.") | |
| # ------------------------------------------------------------ | |
| # ํผ๋ธ๋ฆญ URL: ngrok ์์ ํจ์ | |
| # ------------------------------------------------------------ | |
| def start_ngrok(port=8501, token: str | None = None): | |
| try: | |
| from pyngrok import ngrok, conf | |
| except Exception: | |
| st.error("pyngrok๊ฐ ์ค์น๋์ด ์์ง ์์ต๋๋ค. `pip install pyngrok` ํ ๋ค์ ์๋ํ์ธ์.") | |
| return | |
| # ๊ธฐ์กด ngrok ์ธ์ ์ ๋ฆฌ(์ฌ์คํ ์ ์ถฉ๋ ๋ฐฉ์ง) | |
| try: | |
| ngrok.kill() | |
| time.sleep(1.0) | |
| except Exception: | |
| pass | |
| token = (token or os.environ.get("NGROK_AUTHTOKEN", "")).strip() | |
| if token: | |
| conf.get_default().auth_token = token | |
| else: | |
| st.warning("NGROK_AUTHTOKEN์ด ๋น์ด ์์ต๋๋ค. ์ธ์ฆ ์์ด ์ด๋ฉด ์ ํ/์๋ฌ(4018) ๊ฐ๋ฅ.") | |
| for attempt in range(2): | |
| try: | |
| tunnel = ngrok.connect(addr=f"http://localhost:{port}", proto="http") | |
| url = tunnel.public_url | |
| st.session_state["_ngrok_tunnel"] = tunnel | |
| st.success(f"๐ Public URL: {url}") | |
| st.caption("๋ฐํ์/ํ๋ก์ธ์ค๋ฅผ ์ข ๋ฃํ๋ฉด ํฐ๋๋ ๋ซํ๋๋ค.") | |
| break | |
| except Exception as e: | |
| if attempt == 0: | |
| time.sleep(1.5) | |
| else: | |
| msg = str(e) | |
| if "4018" in msg: | |
| st.error("ngrok ์ธ์ฆ ์คํจ(4018). ํ ํฐ์ ๋ค์ ํ์ธํ์ธ์.") | |
| elif "already online" in msg or "334" in msg: | |
| st.error("๋์ผ ์๋ํฌ์ธํธ๊ฐ ์ด๋ฏธ ์ด๋ ค ์์ต๋๋ค. ์ธ์ ์ฌ์์ ๋๋ ๊ธฐ์กด ํฐ๋ ์ข ๋ฃ ํ ์ฌ์๋.") | |
| else: | |
| st.error(f"ngrok ์ฐ๊ฒฐ ์คํจ: {e}") | |
| # ------------------------------------------------------------ | |
| # ์ฑ ํ์ดํ/ํญ ๊ตฌ์ฑ | |
| # ------------------------------------------------------------ | |
| st.title("ํธ์์ ์์์์ธก & ๋ฐ์ฃผ ์ถ์ฒ โ Pro Suite") | |
| tabs = st.tabs(["โ ๋ฐ์ดํฐ", "โก ํ์ต/๋ชจ๋ธ", "โข ์์ธกยท๋ฐ์ฃผ", "โฃ ๋ถ์(๊ทธ๋ํ)", "โค ์ง๋จ/๋ก๊ทธ"]) | |
| # ============================================================ | |
| # โ ๋ฐ์ดํฐ: CSV ์ ๋ก๋/์ ํ + ์๋ ์ปฌ๋ผ ๋งคํ ์ ์ฅ (๋ฉํฐ CSV ์ง์) | |
| # ============================================================ | |
| with tabs[0]: | |
| st.subheader("CSV ์ ๋ก๋ ๋๋ ์ ํ") | |
| cols_top = st.columns([2,1]) | |
| with cols_top[0]: | |
| add_source = st.checkbox("ํ์ผ๋ช (source) ์ด ์ถ๊ฐ", value=True, help="์ฌ๋ฌ CSV๋ฅผ ํฉ์น ๋ ์๋ณธ ํ์ผ๋ช ์ ๋จ๊น๋๋ค.") | |
| with cols_top[1]: | |
| st.caption("โป ์ ๋ก๋/์ ํ ํ ์๋์์ ์ปฌ๋ผ ๋งคํ ์ ์ฅ") | |
| cols = st.columns(2) | |
| # --- ๋ค์ค ํ์ผ ์ ๋ก๋ --- | |
| with cols[0]: | |
| up_multi = st.file_uploader("CSV ํ์ผ ์ ๋ก๋(์ฌ๋ฌ ๊ฐ ๊ฐ๋ฅ)", type=["csv"], accept_multiple_files=True, key="multi_up") | |
| if up_multi: | |
| dfs = [] | |
| for f in up_multi: | |
| raw = f.read() | |
| df_i = read_csv_flexible(io.BytesIO(raw)) | |
| if add_source: | |
| df_i["source"] = f.name | |
| dfs.append(df_i) | |
| # data/์ ์ ์ฅ | |
| save_path = os.path.join(DATA_DIR, f.name) | |
| try: | |
| with open(save_path, "wb") as fp: | |
| fp.write(raw) | |
| except Exception as e: | |
| st.warning(f"ํ์ผ ์ ์ฅ ๊ฒฝ๊ณ ({f.name}): {e}") | |
| try: | |
| list_data_files.clear() # ์บ์ ๋ฌดํจํ | |
| except Exception: | |
| pass | |
| df = pd.concat(dfs, axis=0, ignore_index=True, sort=True) | |
| st.session_state["df"] = df | |
| st.success(f"์ ๋ก๋/๊ฒฐํฉ ์๋ฃ: {df.shape} (ํ์ผ {len(dfs)}๊ฐ)") | |
| st.dataframe(df.head(20), use_container_width=True) | |
| # --- data ํด๋์์ ๋ค์ค ์ ํ --- | |
| with cols[1]: | |
| files = list_data_files() | |
| picks = st.multiselect("data ํด๋์์ ์ ํ(์ฌ๋ฌ ๊ฐ)", files) | |
| if st.button("์ ํ ํ์ผ ๋ถ๋ฌ์ค๊ธฐ", disabled=(len(picks)==0)): | |
| dfs = [] | |
| for name in picks: | |
| path = os.path.join(DATA_DIR, name) | |
| df_i = read_csv_flexible(path) | |
| if add_source: | |
| df_i["source"] = name | |
| dfs.append(df_i) | |
| df = pd.concat(dfs, axis=0, ignore_index=True, sort=True) | |
| st.session_state["df"] = df | |
| st.success(f"๋ถ๋ฌ์ค๊ธฐ/๊ฒฐํฉ ์๋ฃ: {df.shape} (ํ์ผ {len(dfs)}๊ฐ)") | |
| st.dataframe(df.head(20), use_container_width=True) | |
| # --- ์๋ ์ปฌ๋ผ ๋งคํ + ๋ณด์ --- | |
| if "df" in st.session_state: | |
| st.divider() | |
| st.caption("์๋ ์ปฌ๋ผ ๋งคํ โ ์ ํ ์์ด ์๋ ์ ์ฉ๋ฉ๋๋ค.") | |
| df = st.session_state["df"] | |
| # auto_map_columns ๊ฒฐ๊ณผ ์ฌ์ฉ | |
| auto = auto_map_columns(df) | |
| mapping = { | |
| "date": auto.get("date"), | |
| "target": auto.get("target"), | |
| "region": auto.get("region"), | |
| "brand": auto.get("brand"), | |
| "item": auto.get("item"), | |
| } | |
| st.session_state["mapping"] = mapping | |
| # โ data ํด๋์ฉ ๋ณด์ : | |
| # seoul_gyeonggi_with_demand.csv / usan.csv / gungoguma.csv ๋ | |
| # auto_map_columns๊ฐ ํ๊น์ '๊ฐ์๋'์ผ๋ก ์ก๋ ์ผ์ด์ค๊ฐ ์์ด์, | |
| # '์ผ์ผํ๋งค๋' ์ปฌ๋ผ์ด ์์ผ๋ฉด ๊ทธ๊ฑธ target์ผ๋ก ๊ฐ์ ๊ต์ฒด | |
| if mapping.get("target") == "๊ฐ์๋" and "์ผ์ผํ๋งค๋" in df.columns: | |
| mapping["target"] = "์ผ์ผํ๋งค๋" | |
| # ํ์ธ์ฉ์ผ๋ก๋ง ์ฝ๊ธฐ ์ ์ฉ ํ ์ด๋ธ ํ์ | |
| mapping_view = pd.DataFrame( | |
| { | |
| "์ญํ ": ["๋ ์ง(date)", "์์/ํ๋งค๋(target)", "์ง์ญ/์ ํฌ(region)", "๋ธ๋๋(์ ํ)", "์ํ/ํ๋ชฉ(์ ํ)"], | |
| "์ปฌ๋ผ": [ | |
| mapping.get("date"), | |
| mapping.get("target"), | |
| mapping.get("region"), | |
| mapping.get("brand"), | |
| mapping.get("item"), | |
| ], | |
| } | |
| ) | |
| st.write("ํ์ฌ ์๋ ๋งคํ ๊ฒฐ๊ณผ:") | |
| st.dataframe(mapping_view, use_container_width=True) | |
| # ============================================================ | |
| # โก ํ์ต/๋ชจ๋ธ | |
| # ============================================================ | |
| with tabs[1]: | |
| st.subheader("๋ชจ๋ธ ํ์ต") | |
| use_optuna = st.checkbox("Optuna ํ์ดํผํ๋ผ๋ฏธํฐ ํ๋ ์ฌ์ฉ", value=False) | |
| trials = st.slider("Optuna ์๋ ํ์", 5, 60, 15, 5) | |
| if "df" not in st.session_state or "mapping" not in st.session_state: | |
| st.info("๋จผ์ โ ํญ์์ ๋ฐ์ดํฐ์ ์ปฌ๋ผ ๋งคํ์ ์ง์ ํ์ธ์.") | |
| else: | |
| v = st.slider("๊ฒ์ฆ ๋น์จ(valid_ratio)", 0.05, 0.4, 0.2, 0.05) | |
| if st.button("ํ์ต ์์"): | |
| # โ ์ฌ๊ธฐ์ ์์ธ๊ฐ ๋๋ ์ฑ์ด ์ฃฝ์ง ์๋๋ก ๋ฐฉ์ด | |
| try: | |
| df, X, y, feat_names = make_matrix( | |
| st.session_state["df"], | |
| st.session_state["mapping"], | |
| ) | |
| except Exception as e: | |
| st.error(f"ํ์ต์ฉ ๋ฐ์ดํฐ ๊ตฌ์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}") | |
| else: | |
| try: | |
| best_model, lb = train_and_score( | |
| X, | |
| y, | |
| valid_ratio=v, | |
| use_optuna=use_optuna, | |
| optuna_trials=trials, | |
| ) | |
| save_artifacts( | |
| [ARTI_DIR, MODELS_DIR], | |
| best_model, | |
| feat_names, | |
| st.session_state["mapping"], | |
| lb, | |
| ) | |
| except Exception as e: | |
| st.error(f"๋ชจ๋ธ ํ์ต/์ ์ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}") | |
| else: | |
| st.session_state["leaderboard"] = lb | |
| st.session_state["feat_names"] = feat_names | |
| st.success("ํ์ต ์๋ฃ") | |
| if "leaderboard" in st.session_state: | |
| st.dataframe(st.session_state["leaderboard"], use_container_width=True) | |
| # ============================================================ | |
| # โข ์์ธกยท๋ฐ์ฃผ: ๋ฐ๋ณต(AR) ์์ธก + ์ฌ๊ณ ๊ธฐ๋ฐ ์๋ ๋ฐ์ฃผ ๊ณ์ฐ | |
| # ============================================================ | |
| with tabs[2]: | |
| st.subheader("์์ธก(๋ฐ๋ณต AR) & ๋ฐ์ฃผ๋ ์ถ์ฒ") | |
| st.caption("ํ์ต๋ ๋ชจ๋ธ๋ก ๋ฏธ๋ ํผ์ฒ๋ฅผ ์์ฑํ๊ณ , ์ฌ๊ณ ๋ฅผ ๊ณ ๋ คํด ์๋์ผ๋ก ๋ฐ์ฃผ ๊ธฐ๊ฐ๊ณผ ์๋์ ๊ณ์ฐํฉ๋๋ค.") | |
| if "df" not in st.session_state or "mapping" not in st.session_state: | |
| st.info("๋จผ์ โ ํญ์์ ๋ฐ์ดํฐ์ ์ปฌ๋ผ ๋งคํ์ ์ง์ ํ๊ณ โก์์ ํ์ต์ ์๋ฃํ์ธ์.") | |
| else: | |
| horizon_days = 14 # ๊ณ ์ ๊ธฐ๊ฐ | |
| # ์ ํ๋(๋ณด์ ๊ณ์) | |
| accuracy = st.slider( | |
| "์ ํ๋(์์ธก ๋ณด์ ๊ณ์)", | |
| min_value=0.5, | |
| max_value=2.0, | |
| value=1.0, | |
| step=0.05, | |
| ) | |
| # ============================== | |
| # ์ธ๊ทธ๋จผํธ ์ ํ | |
| # ============================== | |
| seg_cols = [ | |
| c for c in [ | |
| st.session_state["mapping"].get("region"), | |
| st.session_state["mapping"].get("brand"), | |
| st.session_state["mapping"].get("item"), | |
| ] if c | |
| ] | |
| seg_vals = {} | |
| if seg_cols: | |
| col_objs = st.columns(len(seg_cols)) | |
| for i, ccol in enumerate(seg_cols): | |
| opts = ["<์ ์ฒด>"] + sorted( | |
| list(map(str, st.session_state["df"][ccol].dropna().astype(str).unique())) | |
| ) | |
| seg_vals[ccol] = col_objs[i].selectbox(f"{ccol} ์ ํ", opts, index=0) | |
| # ============================== | |
| # ๋ฐ๋ณต ์์ธก ํจ์ | |
| # ============================== | |
| def iterative_forecast(df, mapping, model, feat_names, horizon, seg_vals): | |
| df = df.copy() | |
| dtc = mapping["date"] | |
| tgt = mapping["target"] | |
| if dtc not in df.columns or tgt not in df.columns: | |
| st.error(f"์์ธก์ ํ์ํ ์ปฌ๋ผ์ด ์์ต๋๋ค. (date='{dtc}', target='{tgt}')") | |
| return pd.DataFrame(columns=[dtc, "์์ธก์๋"]) | |
| df[dtc] = pd.to_datetime(df[dtc], errors="coerce") | |
| df = df.dropna(subset=[dtc]).sort_values(dtc) | |
| for k, v in seg_vals.items(): | |
| if v and v != "<์ ์ฒด>" and k in df.columns: | |
| df = df[df[k].astype(str) == str(v)] | |
| if df.empty: | |
| st.error("์ ํํ ์ธ๊ทธ๋จผํธ์ ํด๋นํ๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| return pd.DataFrame(columns=[dtc, "์์ธก์๋"]) | |
| if len(df) < 30: | |
| st.warning("ํด๋น ์ธ๊ทธ๋จผํธ ๋ฐ์ดํฐ๊ฐ ์ ์ด ์์ธก ํ์ง์ด ๋ฎ์ ์ ์์ต๋๋ค.") | |
| last_date = df[dtc].max() | |
| hist = list( | |
| pd.to_numeric(df[tgt], errors="coerce") | |
| .fillna(0) | |
| .astype(float) | |
| .values | |
| ) | |
| def build_row_features(current_date, hist_vals): | |
| if pd.isna(current_date): | |
| current_date = df[dtc].max() | |
| year = current_date.year | |
| month = current_date.month | |
| day = current_date.day | |
| dow = current_date.weekday() | |
| is_weekend = 1 if dow >= 5 else 0 | |
| try: | |
| week = int(pd.Timestamp(current_date).isocalendar().week) | |
| except Exception: | |
| week = 0 | |
| def get_lag(k): | |
| if len(hist_vals) >= k: | |
| return float(hist_vals[-k]) | |
| return float(np.mean(hist_vals[-min(len(hist_vals), 7):])) if hist_vals else 0.0 | |
| lag1 = get_lag(1) | |
| lag7 = get_lag(7) | |
| lag14 = get_lag(14) | |
| def rmean(w): | |
| arr = np.array(hist_vals[-w:]) if len(hist_vals) >= 1 else np.array([0.0]) | |
| if len(arr) < max(2, w // 2): | |
| arr = np.array(hist_vals[-max(2, w // 2):]) if len(hist_vals) else np.array([0.0]) | |
| return float(np.mean(arr)) | |
| def rstd(w): | |
| arr = np.array(hist_vals[-w:]) if len(hist_vals) >= 2 else np.array([0.0, 0.0]) | |
| return float(np.std(arr)) | |
| feats = { | |
| "year": year, | |
| "month": month, | |
| "day": day, | |
| "dow": dow, | |
| "week": week, | |
| "is_weekend": is_weekend, | |
| "lag1": lag1, | |
| "lag7": lag7, | |
| "lag14": lag14, | |
| "rmean7": rmean(7), | |
| "rmean14": rmean(14), | |
| "rstd7": rstd(7), | |
| "rstd14": rstd(14), | |
| } | |
| for fn in feat_names: | |
| if fn not in feats: | |
| feats[fn] = 0.0 | |
| x = [feats.get(fn, 0.0) for fn in feat_names] | |
| return np.array(x, dtype=float) | |
| preds, dates = [], [] | |
| cur = last_date | |
| for _ in range(int(horizon)): | |
| cur = cur + timedelta(days=1) | |
| x = build_row_features(cur, hist) | |
| val = float(model.predict([x])[0]) | |
| preds.append(val) | |
| dates.append(cur) | |
| hist.append(val) | |
| return pd.DataFrame({dtc: dates, "์์ธก์๋": preds}) | |
| # ============================== | |
| # ์ฌ๊ณ ์๋ ์ธ์ | |
| # ============================== | |
| def guess_inventory_onhand(df_seg: pd.DataFrame, mapping): | |
| candidates = [ | |
| "์ฌ๊ณ ", "์ฌ๊ณ ์", "์ฌ๊ณ ์๋", | |
| "ํ์ฌ์ฌ๊ณ ", "onhand", "on_hand", | |
| "stock", "inventory", | |
| ] | |
| inv_col = None | |
| for col in df_seg.columns: | |
| low = col.lower() | |
| if any(key in low for key in candidates): | |
| inv_col = col | |
| break | |
| if not inv_col: | |
| return None, None | |
| series = pd.to_numeric(df_seg[inv_col], errors="coerce").dropna() | |
| if series.empty: | |
| return None, None | |
| return inv_col, float(series.iloc[-1]) | |
| # ============================== | |
| # ๊ฐ๊ฒฉ ์๋ ์ธ์ | |
| # ============================== | |
| def guess_price_column(df_seg): | |
| keys = ["price", "๊ฐ๊ฒฉ", "๋จ๊ฐ", "ํ๋งค๊ฐ", "amount", "๊ธ์ก"] | |
| for col in df_seg.columns: | |
| low = col.lower() | |
| if any(k in low for k in keys): | |
| return col | |
| return None | |
| # ============================== | |
| # ๋ชจ๋ธ ๋ก๋ | |
| # ============================== | |
| pkl_path = os.path.join(MODELS_DIR, "best_model.pkl") | |
| if os.path.exists(pkl_path): | |
| try: | |
| with open(pkl_path, "rb") as f: | |
| payload = pickle.load(f) | |
| model = payload["model"] | |
| feat_names = payload["feature_names"] | |
| mapping = payload["mapping"] | |
| except Exception as e: | |
| st.error(f"์ ์ฅ๋ ๋ชจ๋ธ ๋ก๋ฉ ์ค ์ค๋ฅ: {e}") | |
| else: | |
| dtc = mapping["date"] | |
| # ====================================== | |
| # 1) ์์ธก ์ํ | |
| # ====================================== | |
| fc_df = iterative_forecast( | |
| st.session_state["df"], | |
| mapping, | |
| model, | |
| feat_names, | |
| horizon_days, | |
| seg_vals, | |
| ) | |
| if fc_df.empty: | |
| st.stop() | |
| # ====================================== | |
| # 2) ๊ฐ๊ฒฉ ์๋ ์ธ์ + ๊ธ์ก์์ธก | |
| # ====================================== | |
| df_seg_price = st.session_state["df"].copy() | |
| for k, v in seg_vals.items(): | |
| if v and v != "<์ ์ฒด>" and k in df_seg_price.columns: | |
| df_seg_price = df_seg_price[df_seg_price[k].astype(str) == str(v)] | |
| df_seg_price = df_seg_price.sort_values(dtc) | |
| price_col = guess_price_column(df_seg_price) | |
| if price_col: | |
| price_val = float( | |
| pd.to_numeric(df_seg_price[price_col], errors="coerce").dropna().iloc[-1] | |
| ) | |
| st.info(f"CSV '{price_col}' ์ปฌ๋ผ์์ ๊ฐ๊ฒฉ {price_val:,.0f}์ ์๋ ์ธ์.") | |
| else: | |
| price_val = st.number_input( | |
| "๊ฐ๊ฒฉ(์) โ CSV์์ ๊ฐ๊ฒฉ ์ปฌ๋ผ์ ์ฐพ์ง ๋ชปํด ์ง์ ์ ๋ ฅ", | |
| min_value=0, | |
| max_value=100000000, | |
| value=0, | |
| ) | |
| # **์๋ ์ดํฉ** | |
| total_qty_demand = float(fc_df["์์ธก์๋"].sum()) | |
| # **๊ธ์ก ์ดํฉ** | |
| fc_df["๊ธ์ก์์ธก"] = (fc_df["์์ธก์๋"] * price_val * float(accuracy)).clip(lower=0.0) | |
| total_amt_demand = float(fc_df["๊ธ์ก์์ธก"].sum()) | |
| # ====================================== | |
| # 3) ์ฌ๊ณ ์๋ ์ธ์ | |
| # ====================================== | |
| df_seg = st.session_state["df"].copy() | |
| df_seg[dtc] = pd.to_datetime(df_seg[dtc], errors="coerce") | |
| for k, v in seg_vals.items(): | |
| if v and v != "<์ ์ฒด>" and k in df_seg.columns: | |
| df_seg = df_seg[df_seg[k].astype(str) == str(v)] | |
| df_seg = df_seg.sort_values(dtc) | |
| inv_col, onhand_auto = guess_inventory_onhand(df_seg, mapping) | |
| if onhand_auto is None: | |
| onhand = st.number_input( | |
| "ํ์ฌ ์ฌ๊ณ (์ง์ ์ ๋ ฅ)", | |
| min_value=0, | |
| max_value=100000, | |
| value=0, | |
| ) | |
| else: | |
| onhand = onhand_auto | |
| st.info(f"์ฌ๊ณ '{inv_col}' ์๋ ์ธ์ โ {onhand:,.0f}๊ฐ") | |
| # ====================================== | |
| # 4) ๋ฐ์ฃผ๋/์์ง์ผ ๊ณ์ฐ (์๋ ๊ธฐ์ค) | |
| # ====================================== | |
| avg_daily_qty = total_qty_demand / horizon_days if horizon_days > 0 else 0.0 | |
| days_to_out = (onhand / avg_daily_qty) if avg_daily_qty > 0 else float("inf") | |
| rec_qty = max(0.0, total_qty_demand - onhand) | |
| c1, c2, c3 = st.columns(3) | |
| c1.metric("์์ธก ๊ธฐ๊ฐ(์ผ)", f"{horizon_days}") | |
| c2.metric("์ฌ๊ณ ์์ง ์์์ผ์", "โ" if np.isinf(days_to_out) else f"{days_to_out:,.1f}") | |
| c3.metric("2์ฃผ ์ด ์์ ๋งค์ถ", f"{total_amt_demand:,.0f}์") | |
| # ====================================== | |
| # 5) ํ ์ถ๋ ฅ | |
| # ====================================== | |
| st.dataframe(fc_df.set_index(dtc), use_container_width=True) | |
| st.caption("โป ์์ธก์๋ ร ๊ฐ๊ฒฉ ร ์ ํ๋ ๋ณด์ = ๊ธ์ก์์ธก") | |
| else: | |
| st.warning("best_model.pkl ์ด ์์ต๋๋ค. โก ํญ์์ ํ์ต์ ๋จผ์ ์ํํ์ธ์.") | |
| # ============================================================ | |
| # โฃ ๋ถ์(๊ทธ๋ํ): | |
| # - ์ฐ์ฐ: ํ ๋ฌ ๊ฐ์๋ vs ์ฐ์ฐ ํ๋งค๋ (์ฐ์ ๋ + ํ๊ท์ + ์ผ๋ณ ์ ํ ๊ทธ๋ํ) | |
| # - ๊ตฐ๊ณ ๊ตฌ๋ง: ํ ๋ฌ ๊ธฐ์จ vs ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋ (์ฐ์ ๋ + ํ๊ท์ + ์ผ๋ณ ์ ํ ๊ทธ๋ํ) | |
| # - ์ ์ฒด: ์ฐ์ฐยท๊ตฐ๊ณ ๊ตฌ๋ง ์ ์ธ ์ผ๋ณ ํ๋งค๋ ์ ํ ๊ทธ๋ํ | |
| # ============================================================ | |
| with tabs[3]: | |
| st.subheader("๋ถ์(๊ทธ๋ํ) โ ํ ๋ฌ ๋จ์ ์๊ด ๋ถ์") | |
| if "df" not in st.session_state or "mapping" not in st.session_state or not st.session_state["mapping"].get("date"): | |
| st.info("๋จผ์ โ ํญ์์ ๋ฐ์ดํฐ์ ์ปฌ๋ผ ๋งคํ(ํนํ '๋ ์ง'์ 'ํ๊น')์ ์ง์ ํ์ธ์.") | |
| else: | |
| mapping = st.session_state["mapping"] | |
| date_col = mapping["date"] | |
| target_col = mapping.get("target") | |
| def guess(colnames, cands): | |
| low = [str(c).lower() for c in colnames] | |
| for key in cands: | |
| key_low = str(key).lower() | |
| for i, l in enumerate(low): | |
| if key_low in l: | |
| return colnames[i] | |
| return None | |
| # ๊ณตํต: ์ฐ-์ ์ ํ์ฉ ์ต์ ๋ง๋๋ ํจ์ | |
| def build_year_month_options(df, date_col): | |
| df = df.copy() | |
| df[date_col] = pd.to_datetime(df[date_col], errors="coerce") | |
| df = df.dropna(subset=[date_col]) | |
| if df.empty: | |
| return df, [] | |
| df["year_month"] = df[date_col].dt.to_period("M") | |
| ym_unique = sorted(df["year_month"].unique()) | |
| ym_labels = [str(p) for p in ym_unique] # '2024-10' ๊ฐ์ ํ์ | |
| return df, list(zip(ym_labels, ym_unique)) | |
| tab_u, tab_g, tab_all = st.tabs([ | |
| "โ ์ฐ์ฐ: ํ ๋ฌ ๊ฐ์๋ vs ํ๋งค๋", | |
| "๐ ๊ตฐ๊ณ ๊ตฌ๋ง: ํ ๋ฌ ๊ธฐ์จ vs ํ๋งค๋", | |
| "๐ ์ ์ฒด: ์ฐ์ฐยท๊ตฐ๊ณ ๊ตฌ๋ง ์ ์ธ ์ผ๋ณ ํ๋งค๋(์ ํ)" | |
| ]) | |
| # ------------------------------ | |
| # 1) ์ฐ์ฐ: ์ ํํ ํ ๋ฌ์ ๊ฐ์๋ โ ์ฐ์ฐ ํ๋งค๋ | |
| # ------------------------------ | |
| with tab_u: | |
| st.caption("์ฐ์ฐ ํ๋งค๋๊ณผ ๊ฐ์๋์ ๊ด๊ณ๋ฅผ 'ํ ๋ฌ' ๋จ์๋ก ๋ด ๋๋ค.") | |
| up_u = st.file_uploader("์ฐ์ฐ/๋ ์จ ๋ฐ์ดํฐ CSV (์ ํ)", type=["csv"], key="umbrella_month_up") | |
| if up_u is not None: | |
| df_u_raw = read_csv_flexible(io.BytesIO(up_u.read())) | |
| else: | |
| df_u_raw = st.session_state["df"].copy() | |
| if date_col not in df_u_raw.columns: | |
| st.warning(f"๋ ์ง ์ปฌ๋ผ '{date_col}' ์(๋ฅผ) ๋ฐ์ดํฐ์์ ์ฐพ์ง ๋ชปํ์ต๋๋ค.") | |
| else: | |
| # item์์ ์ฐ์ฐ๋ง ํํฐ (์์ผ๋ฉด) | |
| item_col = mapping.get("item") | |
| if item_col and item_col in df_u_raw.columns: | |
| mask = df_u_raw[item_col].astype(str).str.contains("์ฐ์ฐ|umbrella", case=False, na=False) | |
| if mask.any(): | |
| df_u_raw = df_u_raw[mask] | |
| cols_all = list(df_u_raw.columns) | |
| # ํ๋งค๋ ์ปฌ๋ผ: ๋งคํ target ์ฐ์ , ์์ผ๋ฉด ์ถ์ | |
| sales_col = target_col if target_col in cols_all else guess( | |
| cols_all, | |
| ["umbrella", "์ฐ์ฐ", "์ผ์ผํ๋งค๋", "ํ๋งค๋", "sales", "qty", "quantity", "target"], | |
| ) | |
| # ๊ฐ์๋ ์ปฌ๋ผ ์ถ์ | |
| rain_col = guess( | |
| cols_all, | |
| ["rain", "precip", "precipitation", "๊ฐ์", "๊ฐ์๋", "์ผ๊ฐ์๋", "๊ฐ์ฐ", "๊ฐ์ฐ๋"], | |
| ) | |
| if not sales_col or not rain_col: | |
| st.warning( | |
| "์ฐ์ฐ ํ๋งค๋ ๋๋ ๊ฐ์๋ ์ปฌ๋ผ์ ์๋์ผ๋ก ์ฐพ์ง ๋ชปํ์ต๋๋ค.\n" | |
| "ํ๋งค๋: '์ฐ์ฐ/umbrella/ํ๋งค๋/sales', ๊ฐ์๋: '๊ฐ์๋/rain' ๋ฑ์ ์ด๋ฆ์ ์ฌ์ฉํด ์ฃผ์ธ์." | |
| ) | |
| else: | |
| # ๋ ์ง/์ซ์ ํ์ ์ ๋ฆฌ + ์ฐ-์ ์ต์ ์์ฑ | |
| df_u_raw[sales_col] = pd.to_numeric(df_u_raw[sales_col], errors="coerce") | |
| df_u_raw[rain_col] = pd.to_numeric(df_u_raw[rain_col], errors="coerce") | |
| df_u_raw, ym_options = build_year_month_options(df_u_raw, date_col) | |
| if not ym_options: | |
| st.info("์ ํจํ ๋ ์ง ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| else: | |
| # ์ฐ-์ ์ ํ (YYYY-MM ํ์๋ง ๋ณด์ฌ์ค) | |
| labels = [lab for lab, _ in ym_options] | |
| default_idx = len(labels) - 1 # ๊ธฐ๋ณธ๊ฐ: ๊ฐ์ฅ ์ต๊ทผ ์ | |
| sel_label = st.selectbox("๋ถ์ํ ์ฐ์(YYYY-MM)", labels, index=default_idx, key="ym_umbrella") | |
| sel_period = dict(ym_options)[sel_label] | |
| # ์ ํํ ํ ๋ฌ๋ง ํํฐ | |
| df_month = df_u_raw[df_u_raw["year_month"] == sel_period].copy() | |
| if df_month.empty: | |
| st.info(f"{sel_label} ์ ํด๋นํ๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| else: | |
| # ์ผ ๋จ์ ์ง๊ณ | |
| df_month["date_only"] = df_month[date_col].dt.date | |
| daily = ( | |
| df_month.groupby("date_only", as_index=False) | |
| .agg({sales_col: "sum", rain_col: "mean"}) | |
| .dropna(subset=[sales_col, rain_col]) | |
| ) | |
| daily = daily.rename( | |
| columns={"date_only": "date", sales_col: "sales", rain_col: "rain"} | |
| ) | |
| if daily.empty: | |
| st.info("ํด๋น ์ฐ์์์ ์ผ๋ณ๋ก ์ง๊ณํ ์ ์๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| else: | |
| st.markdown(f"**{sel_label} ํ ๋ฌ ๊ธฐ์ค ยท ๊ฐ์๋์ ๋ฐ๋ฅธ ์ฐ์ฐ ํ๋งค๋**") | |
| base = alt.Chart(daily).encode( | |
| x=alt.X("rain:Q", title="์ผ ๊ฐ์๋"), | |
| y=alt.Y("sales:Q", title="์ผ ์ฐ์ฐ ํ๋งค๋"), | |
| ) | |
| # ๋ถ์์ ์ฐ์ ๋ + ์ ํ ํ๊ท์ | |
| points = base.mark_circle(size=70, color="#d62728").encode( | |
| tooltip=[ | |
| alt.Tooltip("date:T", title="๋ ์ง"), | |
| alt.Tooltip("rain:Q", title="๊ฐ์๋"), | |
| alt.Tooltip("sales:Q", title="์ฐ์ฐ ํ๋งค๋"), | |
| ] | |
| ) | |
| reg_line = base.transform_regression("rain", "sales").mark_line(color="#b22222") | |
| st.altair_chart((points + reg_line).interactive(), use_container_width=True) | |
| # โ ์ถ๊ฐ: ์ผ๋ณ ์ฐ์ฐ ํ๋งค๋ ์ ํ ๊ทธ๋ํ | |
| st.markdown("**์ผ๋ณ ์ฐ์ฐ ํ๋งค๋ ์ถ์ธ(์ ํ ๊ทธ๋ํ)**") | |
| line_umbrella = ( | |
| alt.Chart(daily) | |
| .mark_line() | |
| .encode( | |
| x=alt.X("date:T", title="๋ ์ง"), | |
| y=alt.Y("sales:Q", title="์ผ ์ฐ์ฐ ํ๋งค๋"), | |
| tooltip=[ | |
| alt.Tooltip("date:T", title="๋ ์ง"), | |
| alt.Tooltip("sales:Q", title="์ฐ์ฐ ํ๋งค๋"), | |
| alt.Tooltip("rain:Q", title="๊ฐ์๋"), | |
| ], | |
| ) | |
| ) | |
| st.altair_chart(line_umbrella.interactive(), use_container_width=True) | |
| # ์ฐธ๊ณ ์ฉ ํ ์ด๋ธ | |
| st.dataframe(daily, use_container_width=True) | |
| # ------------------------------ | |
| # 2) ๊ตฐ๊ณ ๊ตฌ๋ง: ์ ํํ ํ ๋ฌ์ ๊ธฐ์จ โ ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋ | |
| # ------------------------------ | |
| with tab_g: | |
| st.caption("๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋๊ณผ ๊ธฐ์จ(์ถ์)์ ๊ด๊ณ๋ฅผ 'ํ ๋ฌ' ๋จ์๋ก ๋ด ๋๋ค.") | |
| up_g = st.file_uploader("๊ตฐ๊ณ ๊ตฌ๋ง/๋ ์จ ๋ฐ์ดํฐ CSV (์ ํ)", type=["csv"], key="goguma_month_up") | |
| if up_g is not None: | |
| df_g_raw = read_csv_flexible(io.BytesIO(up_g.read())) | |
| else: | |
| df_g_raw = st.session_state["df"].copy() | |
| if date_col not in df_g_raw.columns: | |
| st.warning(f"๋ ์ง ์ปฌ๋ผ '{date_col}' ์(๋ฅผ) ๋ฐ์ดํฐ์์ ์ฐพ์ง ๋ชปํ์ต๋๋ค.") | |
| else: | |
| # item์์ ๊ตฐ๊ณ ๊ตฌ๋ง๋ง ํํฐ (์์ผ๋ฉด) | |
| item_col_g = mapping.get("item") | |
| if item_col_g and item_col_g in df_g_raw.columns: | |
| mask_g = df_g_raw[item_col_g].astype(str).str.contains( | |
| "๊ณ ๊ตฌ๋ง|๊ตฐ๊ณ ๊ตฌ๋ง|sweet|goguma", case=False, na=False | |
| ) | |
| if mask_g.any(): | |
| df_g_raw = df_g_raw[mask_g] | |
| cols_all_g = list(df_g_raw.columns) | |
| goguma_col = target_col if target_col in cols_all_g else guess( | |
| cols_all_g, | |
| ["๊ณ ๊ตฌ๋ง", "๊ตฐ๊ณ ๊ตฌ๋ง", "sweetpotato", "goguma", "ํ๋งค๋", "sales", "qty", "quantity", "target"], | |
| ) | |
| temp_col = guess( | |
| cols_all_g, | |
| ["์จ๋", "tmin", "temp_min", "min_temp", "์ต์ ", "์ต์ ๊ธฐ์จ", "์ผ์ต์ ๊ธฐ์จ", "temperature", "temp"], | |
| ) | |
| if not goguma_col or not temp_col: | |
| st.warning( | |
| "๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋ ๋๋ ๊ธฐ์จ ์ปฌ๋ผ์ ์๋์ผ๋ก ์ฐพ์ง ๋ชปํ์ต๋๋ค.\n" | |
| "ํ๋งค๋: '๊ตฐ๊ณ ๊ตฌ๋ง/๊ณ ๊ตฌ๋ง/sales/target', ๊ธฐ์จ: 'tmin/์ต์ ๊ธฐ์จ/temperature' ๋ฑ์ ์ด๋ฆ์ ์ฌ์ฉํด ์ฃผ์ธ์." | |
| ) | |
| else: | |
| df_g_raw[goguma_col] = pd.to_numeric(df_g_raw[goguma_col], errors="coerce") | |
| df_g_raw[temp_col] = pd.to_numeric(df_g_raw[temp_col], errors="coerce") | |
| df_g_raw, ym_options_g = build_year_month_options(df_g_raw, date_col) | |
| if not ym_options_g: | |
| st.info("์ ํจํ ๋ ์ง ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| else: | |
| labels_g = [lab for lab, _ in ym_options_g] | |
| default_idx_g = len(labels_g) - 1 | |
| sel_label_g = st.selectbox("๋ถ์ํ ์ฐ์(YYYY-MM)", labels_g, index=default_idx_g, key="ym_goguma") | |
| sel_period_g = dict(ym_options_g)[sel_label_g] | |
| df_month_g = df_g_raw[df_g_raw["year_month"] == sel_period_g].copy() | |
| if df_month_g.empty: | |
| st.info(f"{sel_label_g} ์ ํด๋นํ๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| else: | |
| df_month_g["date_only"] = df_month_g[date_col].dt.date | |
| daily_g = ( | |
| df_month_g.groupby("date_only", as_index=False) | |
| .agg({goguma_col: "sum", temp_col: "mean"}) | |
| .dropna(subset=[goguma_col, temp_col]) | |
| ) | |
| daily_g = daily_g.rename( | |
| columns={"date_only": "date", goguma_col: "sales", temp_col: "temp"} | |
| ) | |
| if daily_g.empty: | |
| st.info("ํด๋น ์ฐ์์์ ์ผ๋ณ๋ก ์ง๊ณํ ์ ์๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| else: | |
| st.markdown(f"**{sel_label_g} ํ ๋ฌ ๊ธฐ์ค ยท ๊ธฐ์จ์ ๋ฐ๋ฅธ ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋**") | |
| base_g = alt.Chart(daily_g).encode( | |
| x=alt.X("temp:Q", title="์ผ ํ๊ท ๊ธฐ์จ"), | |
| y=alt.Y("sales:Q", title="์ผ ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋"), | |
| ) | |
| points_g = base_g.mark_circle(size=70, color="#ff7f0e").encode( | |
| tooltip=[ | |
| alt.Tooltip("date:T", title="๋ ์ง"), | |
| alt.Tooltip("temp:Q", title="๊ธฐ์จ"), | |
| alt.Tooltip("sales:Q", title="๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋"), | |
| ] | |
| ) | |
| reg_g = base_g.transform_regression("temp", "sales").mark_line(color="#d35400") | |
| st.altair_chart((points_g + reg_g).interactive(), use_container_width=True) | |
| # โ ์ถ๊ฐ: ์ผ๋ณ ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋ ์ ํ ๊ทธ๋ํ | |
| st.markdown("**์ผ๋ณ ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋ ์ถ์ธ(์ ํ ๊ทธ๋ํ)**") | |
| line_goguma = ( | |
| alt.Chart(daily_g) | |
| .mark_line() | |
| .encode( | |
| x=alt.X("date:T", title="๋ ์ง"), | |
| y=alt.Y("sales:Q", title="์ผ ๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋"), | |
| tooltip=[ | |
| alt.Tooltip("date:T", title="๋ ์ง"), | |
| alt.Tooltip("temp:Q", title="๊ธฐ์จ"), | |
| alt.Tooltip("sales:Q", title="๊ตฐ๊ณ ๊ตฌ๋ง ํ๋งค๋"), | |
| ], | |
| ) | |
| ) | |
| st.altair_chart(line_goguma.interactive(), use_container_width=True) | |
| st.dataframe(daily_g, use_container_width=True) | |
| # ------------------------------ | |
| # 3) ์ ์ฒด: ์ฐ์ฐยท๊ตฐ๊ณ ๊ตฌ๋ง ์ ์ธ ์ ์ฒด ์ํ ์ผ๋ณ ํ๋งค๋ ์ ํ ๊ทธ๋ํ | |
| # ------------------------------ | |
| with tab_all: | |
| st.caption("์ฐ์ฐยท๊ตฐ๊ณ ๊ตฌ๋ง๋ฅผ ์ ์ธํ ๋ชจ๋ ์ํ์ ์ผ๋ณ ํ๋งค๋ ์ถ์ธ๋ฅผ ํ ๋ฒ์ ๋ด ๋๋ค.") | |
| df_all = st.session_state["df"].copy() | |
| if date_col not in df_all.columns or not target_col or target_col not in df_all.columns: | |
| st.warning(f"๋ ์ง('{date_col}') ๋๋ ํ๊น('{target_col}') ์ปฌ๋ผ์ ์ฐพ์ ์ ์์ต๋๋ค.") | |
| else: | |
| # item ์ปฌ๋ผ์ด ์์ผ๋ฉด ์ฐ์ฐ/๊ตฐ๊ณ ๊ตฌ๋ง ๊ด๋ จ ์ํ ์ ์ธ | |
| item_col_all = mapping.get("item") | |
| if item_col_all and item_col_all in df_all.columns: | |
| ex_mask = df_all[item_col_all].astype(str).str.contains( | |
| "์ฐ์ฐ|umbrella|๊ณ ๊ตฌ๋ง|๊ตฐ๊ณ ๊ตฌ๋ง|sweet|goguma", case=False, na=False | |
| ) | |
| df_all = df_all[~ex_mask] | |
| df_all[target_col] = pd.to_numeric(df_all[target_col], errors="coerce") | |
| df_all, ym_options_all = build_year_month_options(df_all, date_col) | |
| if not ym_options_all: | |
| st.info("์ ํจํ ๋ ์ง ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| else: | |
| labels_all = [lab for lab, _ in ym_options_all] | |
| default_idx_all = len(labels_all) - 1 | |
| sel_label_all = st.selectbox( | |
| "๋ถ์ํ ์ฐ์(YYYY-MM)", | |
| labels_all, | |
| index=default_idx_all, | |
| key="ym_all", | |
| ) | |
| sel_period_all = dict(ym_options_all)[sel_label_all] | |
| df_month_all = df_all[df_all["year_month"] == sel_period_all].copy() | |
| if df_month_all.empty: | |
| st.info(f"{sel_label_all} ์ ํด๋นํ๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| else: | |
| df_month_all["date_only"] = df_month_all[date_col].dt.date | |
| daily_all = ( | |
| df_month_all.groupby("date_only", as_index=False) | |
| .agg({target_col: "sum"}) | |
| .dropna(subset=[target_col]) | |
| ) | |
| daily_all = daily_all.rename( | |
| columns={"date_only": "date", target_col: "sales"} | |
| ) | |
| if daily_all.empty: | |
| st.info("ํด๋น ์ฐ์์์ ์ผ๋ณ๋ก ์ง๊ณํ ์ ์๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| else: | |
| st.markdown(f"**{sel_label_all} ํ ๋ฌ ๊ธฐ์ค ยท ์ฐ์ฐยท๊ตฐ๊ณ ๊ตฌ๋ง ์ ์ธ ์ ์ฒด ์ํ ์ผ๋ณ ํ๋งค๋(์ ํ)**") | |
| line_all = ( | |
| alt.Chart(daily_all) | |
| .mark_line() | |
| .encode( | |
| x=alt.X("date:T", title="๋ ์ง"), | |
| y=alt.Y("sales:Q", title="์ผ ํ๋งค๋(์ ์ฒด ์ํ ํฉ๊ณ)"), | |
| tooltip=[ | |
| alt.Tooltip("date:T", title="๋ ์ง"), | |
| alt.Tooltip("sales:Q", title="์ผ ํ๋งค๋ ํฉ๊ณ"), | |
| ], | |
| ) | |
| ) | |
| st.altair_chart(line_all.interactive(), use_container_width=True) | |
| st.dataframe(daily_all, use_container_width=True) | |
| # ============================================================ | |
| # โค ์ง๋จ/๋ก๊ทธ: ๊ฒฝ๋ก/ํ์ผ ํ์ธ + ํผ๋ธ๋ฆญ URL ์ด๊ธฐ/๋ซ๊ธฐ | |
| # ============================================================ | |
| with tabs[4]: | |
| st.subheader("๊ฒฝ๋ก/ํ์ผ ์ํ") | |
| cols = st.columns(2) | |
| with cols[0]: | |
| st.write("**data**", DATA_DIR) | |
| st.write(os.listdir(DATA_DIR) if os.path.exists(DATA_DIR) else []) | |
| st.write("**artifacts**", ARTI_DIR) | |
| st.write(os.listdir(ARTI_DIR) if os.path.exists(ARTI_DIR) else []) | |
| with cols[1]: | |
| st.write("**models**", MODELS_DIR) | |
| st.write(os.listdir(MODELS_DIR) if os.path.exists(MODELS_DIR) else []) | |
| st.caption("ํ์ ์ ํผ๋ธ๋ฆญ URL์ ์ด์ด ์ธ๋ถ์์ ์ ์ํ ์ ์์ต๋๋ค.") | |
| mode = st.radio("ํผ๋ธ๋ฆญ URL ํฐ๋๋ฌ", ["ngrok", "cloudflared"], horizontal=True, index=0) | |
| ngk = None | |
| if mode == "ngrok": | |
| ngk = st.text_input( | |
| "NGROK_AUTHTOKEN", | |
| value=os.environ.get("NGROK_AUTHTOKEN", ""), | |
| type="password", | |
| help="ํ๊ฒฝ๋ณ์์ ๋ฃ์ด๋๋ฉด ๋ค์๋ถํฐ ์๋ ์ธ์ํฉ๋๋ค.", | |
| ) | |
| c_open, c_close = st.columns(2) | |
| if c_open.button("ํผ๋ธ๋ฆญ URL ์ด๊ธฐ", use_container_width=True): | |
| if mode == "ngrok": | |
| if ngk: | |
| os.environ["NGROK_AUTHTOKEN"] = ngk | |
| start_ngrok() | |
| else: | |
| start_cloudflared() | |
| if c_close.button("ํผ๋ธ๋ฆญ URL ๋ซ๊ธฐ", use_container_width=True): | |
| if mode == "ngrok": | |
| try: | |
| from pyngrok import ngrok | |
| ngrok.kill() | |
| st.info("ngrok ํฐ๋์ ์ข ๋ฃํ์ต๋๋ค.") | |
| except Exception as e: | |
| st.warning(f"ngrok ์ข ๋ฃ ์ค ๊ฒฝ๊ณ : {e}") | |
| else: | |
| proc = st.session_state.get("_cfd_proc") | |
| if proc: | |
| proc.terminate() | |
| st.info("cloudflared ํฐ๋์ ์ข ๋ฃํ์ต๋๋ค.") | |
| else: | |
| st.info("cloudflared ํ์ฑ ํ๋ก์ธ์ค๊ฐ ์์ต๋๋ค.") | |