| from pathlib import Path |
|
|
| import pandas as pd |
| import streamlit as st |
|
|
| st.set_page_config( |
| page_title="๊ณ๋๊ธฐ ๊ต์ฒด ํ๋ณด ๊ฒํ ์ฑ", |
| layout="wide", |
| ) |
|
|
| DATA_FILE = Path("/04_๊ณ๋๊ธฐ_๊ต์ฒด๋์_์๋์ถ์ถ_์์๋ฐ์ดํฐ.xlsx") |
| SHEET_NAME = "๊ณ๋๊ธฐ_๊ต์ฒด๋์" |
|
|
| REQUIRED_COLUMNS = [ |
| "๊ณ๋๊ธฐID", |
| "๊ณ ๊ฐID", |
| "์๋น์ค์ผํฐ", |
| "์ง์ญ", |
| "์ค์น์ผ์", |
| "์ฌ์ฉ์ฐ์", |
| "๋ฐฐํฐ๋ฆฌ์๋_pct", |
| "์ต๊ทผ์ค๋ฅ๊ฑด์", |
| "ํต์ ์ํ", |
| "๋ถํ์ฌ๊ณ ์ฌ๋ถ", |
| "์ฐธ๊ณ _๊ต์ฒด์ ์", |
| "์ฐธ๊ณ _๊ต์ฒด๋ฑ๊ธ", |
| "๊ต์ฒด์ฌ์ ", |
| "๊ถ์ฅ์กฐ์น", |
| ] |
|
|
|
|
| @st.cache_data |
| def load_default_excel(file_path: str, sheet_name: str) -> pd.DataFrame: |
| return pd.read_excel(file_path, sheet_name=sheet_name) |
|
|
|
|
| def load_excel(uploaded_file, default_file: Path, sheet_name: str) -> pd.DataFrame: |
| if uploaded_file is not None: |
| return pd.read_excel(uploaded_file, sheet_name=sheet_name) |
|
|
| if default_file.exists(): |
| return load_default_excel(str(default_file), sheet_name) |
|
|
| return pd.DataFrame() |
|
|
|
|
| def prepare_data(df: pd.DataFrame) -> pd.DataFrame: |
| df = df.copy() |
|
|
| df["์ค์น์ผ์"] = pd.to_datetime(df["์ค์น์ผ์"], errors="coerce") |
|
|
| number_columns = [ |
| "์ฌ์ฉ์ฐ์", |
| "๋ฐฐํฐ๋ฆฌ์๋_pct", |
| "์ต๊ทผ์ค๋ฅ๊ฑด์", |
| "์ฐธ๊ณ _๊ต์ฒด์ ์", |
| ] |
|
|
| for col in number_columns: |
| df[col] = pd.to_numeric(df[col], errors="coerce") |
|
|
| return df |
|
|
|
|
| def get_missing_columns(df: pd.DataFrame) -> list: |
| return [col for col in REQUIRED_COLUMNS if col not in df.columns] |
|
|
|
|
| def to_csv_bytes(df: pd.DataFrame) -> bytes: |
| return df.to_csv(index=False).encode("utf-8-sig") |
|
|
|
|
| def has_stock(value) -> bool: |
| text = str(value).strip().upper() |
| return text in ["์", "Y", "YES", "O", "OK", "์์", "์๋ค", "๊ฐ๋ฅ", "๋ณด์ ", "์ฌ๊ณ ์์"] |
|
|
|
|
| st.title("๊ณ๋๊ธฐ ๊ต์ฒด ํ๋ณด ๊ฒํ ์ฑ") |
| st.caption( |
| "๊ณ๋๊ธฐ ๊ต์ฒด ๋์ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ์ค๊ฐ์ผ๋ก ํํฐ๋งํ๊ณ , " |
| "๋ด๋น์๊ฐ ์ ํํ ๊ต์ฒด ํ๋ณด๋ฅผ ๋ค์ด๋ก๋ํ๋ ์
๋ฌดํ Streamlit ์ฑ์
๋๋ค." |
| ) |
|
|
| st.sidebar.header("๋ฐ์ดํฐ ์
๋ก๋") |
|
|
| uploaded_file = st.sidebar.file_uploader( |
| "๊ณ๋๊ธฐ ๊ต์ฒด ๋์ ์์
", |
| type=["xlsx"], |
| ) |
|
|
| meter = load_excel(uploaded_file, DATA_FILE, SHEET_NAME) |
|
|
| if meter.empty: |
| st.warning( |
| "๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค์ง ๋ชปํ์ต๋๋ค. " |
| "`data/04_๊ณ๋๊ธฐ_๊ต์ฒด๋์_์๋์ถ์ถ_์์๋ฐ์ดํฐ.xlsx` ํ์ผ์ด ์๋์ง ํ์ธํ๊ฑฐ๋ " |
| "์์
ํ์ผ์ ์
๋ก๋ํด ์ฃผ์ธ์." |
| ) |
| st.stop() |
|
|
| missing_columns = get_missing_columns(meter) |
|
|
| if missing_columns: |
| st.error("ํ์ ์ปฌ๋ผ์ด ๋๋ฝ๋์ด ์ฑ์ ์คํํ ์ ์์ต๋๋ค.") |
| st.write("๋๋ฝ๋ ์ปฌ๋ผ:", missing_columns) |
| st.write("ํ์ฌ ์์
์ปฌ๋ผ:", meter.columns.tolist()) |
| st.stop() |
|
|
| meter = prepare_data(meter) |
|
|
| center_options = sorted(meter["์๋น์ค์ผํฐ"].dropna().unique().tolist()) |
| grade_options = sorted(meter["์ฐธ๊ณ _๊ต์ฒด๋ฑ๊ธ"].dropna().unique().tolist()) |
| status_options = sorted(meter["ํต์ ์ํ"].dropna().unique().tolist()) |
|
|
| st.sidebar.header("์กฐ๊ฑด ์กฐ์ ") |
|
|
| with st.sidebar.form("criteria_form"): |
| selected_centers = st.multiselect( |
| "์๋น์ค์ผํฐ", |
| options=center_options, |
| default=[], |
| ) |
|
|
| selected_grades = st.multiselect( |
| "๊ต์ฒด๋ฑ๊ธ", |
| options=grade_options, |
| default=[], |
| ) |
|
|
| selected_status = st.multiselect( |
| "ํต์ ์ํ", |
| options=status_options, |
| default=[], |
| ) |
|
|
| min_replace_score = st.slider( |
| "๊ต์ฒด ์ต์ ์ ์", |
| min_value=0, |
| max_value=100, |
| value=70, |
| step=5, |
| ) |
|
|
| max_battery = st.slider( |
| "๋ฐฐํฐ๋ฆฌ ์๋ ๊ธฐ์ค ์ดํ(%)", |
| min_value=0, |
| max_value=100, |
| value=20, |
| step=5, |
| ) |
|
|
| stock_only = st.checkbox( |
| "๋ถํ ์ฌ๊ณ ๊ฐ ์๋ ๊ฑด๋ง ๋ณด๊ธฐ", |
| value=True, |
| ) |
|
|
| daily_target = st.number_input( |
| "์ค๋ ์ฒ๋ฆฌ ๋ชฉํ ๊ฑด์", |
| min_value=1, |
| max_value=100, |
| value=20, |
| step=1, |
| ) |
|
|
| st.form_submit_button("์กฐ๊ฑด ์ ์ฉ") |
|
|
| candidates = meter.copy() |
|
|
| if selected_centers: |
| candidates = candidates[candidates["์๋น์ค์ผํฐ"].isin(selected_centers)] |
|
|
| if selected_grades: |
| candidates = candidates[candidates["์ฐธ๊ณ _๊ต์ฒด๋ฑ๊ธ"].isin(selected_grades)] |
|
|
| if selected_status: |
| candidates = candidates[candidates["ํต์ ์ํ"].isin(selected_status)] |
|
|
| if stock_only: |
| candidates = candidates[candidates["๋ถํ์ฌ๊ณ ์ฌ๋ถ"].apply(has_stock)] |
|
|
| candidates = candidates[ |
| (candidates["์ฐธ๊ณ _๊ต์ฒด์ ์"] >= min_replace_score) |
| & (candidates["๋ฐฐํฐ๋ฆฌ์๋_pct"] <= max_battery) |
| ].copy() |
|
|
| candidates = candidates.sort_values( |
| by=["์ฐธ๊ณ _๊ต์ฒด์ ์", "๋ฐฐํฐ๋ฆฌ์๋_pct", "์ต๊ทผ์ค๋ฅ๊ฑด์", "์ฌ์ฉ์ฐ์"], |
| ascending=[False, True, False, False], |
| ) |
|
|
| replace_count = int(len(candidates)) |
| immediate_count = int(candidates["์ฐธ๊ณ _๊ต์ฒด๋ฑ๊ธ"].isin(["์ฆ์๊ต์ฒด"]).sum()) |
| avg_replace_score = candidates["์ฐธ๊ณ _๊ต์ฒด์ ์"].mean() |
| stock_count = int(candidates["๋ถํ์ฌ๊ณ ์ฌ๋ถ"].apply(has_stock).sum()) |
|
|
| m1, m2, m3, m4 = st.columns(4) |
|
|
| m1.metric("๊ต์ฒด ํ๋ณด ์", f"{replace_count:,}๊ฑด") |
| m2.metric("์ฆ์๊ต์ฒด ํ๋ณด ์", f"{immediate_count:,}๊ฑด") |
| m3.metric( |
| "ํ๊ท ๊ต์ฒด ์ ์", |
| f"{avg_replace_score:.1f}์ " if replace_count else "0.0์ ", |
| ) |
| m4.metric("์ฌ๊ณ ์๋ ํ๋ณด ์", f"{stock_count:,}๊ฑด") |
|
|
| candidate_tab, selected_tab, guide_tab = st.tabs( |
| ["๊ณ๋๊ธฐ ๊ต์ฒด ํ๋ณด", "์ ํ ๊ฒฐ๊ณผ", "๋ฐ์ดํฐ ์ค๋ช
"] |
| ) |
|
|
| with candidate_tab: |
| st.subheader("๊ณ๋๊ธฐ ๊ต์ฒด ํ๋ณด") |
| st.write("์กฐ๊ฑด์ ํต๊ณผํ ๊ณ๋๊ธฐ ๊ต์ฒด ํ๋ณด๋ฅผ ํ์ธํ๊ณ , ๋ด๋น์๊ฐ ์ง์ ์ ํ ์ฌ๋ถ์ ๋ฉ๋ชจ๋ฅผ ์
๋ ฅํฉ๋๋ค.") |
|
|
| display_columns = [ |
| "๊ณ๋๊ธฐID", |
| "๊ณ ๊ฐID", |
| "์๋น์ค์ผํฐ", |
| "์ง์ญ", |
| "์ค์น์ผ์", |
| "์ฌ์ฉ์ฐ์", |
| "๋ฐฐํฐ๋ฆฌ์๋_pct", |
| "์ต๊ทผ์ค๋ฅ๊ฑด์", |
| "ํต์ ์ํ", |
| "๋ถํ์ฌ๊ณ ์ฌ๋ถ", |
| "์ฐธ๊ณ _๊ต์ฒด์ ์", |
| "์ฐธ๊ณ _๊ต์ฒด๋ฑ๊ธ", |
| "๊ต์ฒด์ฌ์ ", |
| "๊ถ์ฅ์กฐ์น", |
| ] |
|
|
| candidate_table = candidates[display_columns].copy() |
| candidate_table.insert(0, "์ ํ", False) |
| candidate_table["๋ด๋น๋ฉ๋ชจ"] = "" |
|
|
| if candidate_table.empty: |
| st.info("ํ์ฌ ์กฐ๊ฑด์ ๋ง๋ ๊ณ๋๊ธฐ ๊ต์ฒด ํ๋ณด๊ฐ ์์ต๋๋ค. ๊ธฐ์ค์ ๋ฎ์ถ๊ฑฐ๋ ํํฐ ์กฐ๊ฑด์ ์กฐ์ ํด ์ฃผ์ธ์.") |
| edited_candidates = candidate_table |
| else: |
| edited_candidates = st.data_editor( |
| candidate_table, |
| use_container_width=True, |
| hide_index=True, |
| num_rows="fixed", |
| disabled=[ |
| col |
| for col in candidate_table.columns |
| if col not in ["์ ํ", "๋ด๋น๋ฉ๋ชจ"] |
| ], |
| column_config={ |
| "์ ํ": st.column_config.CheckboxColumn( |
| "์ ํ", |
| help="์ต์ข
๊ต์ฒด ํ๋ณด๋ก ์ ํํ ๊ฒฝ์ฐ ์ฒดํฌํฉ๋๋ค.", |
| ), |
| "๋ด๋น๋ฉ๋ชจ": st.column_config.TextColumn( |
| "๋ด๋น๋ฉ๋ชจ", |
| help="๊ต์ฒด ์ ํ์ธ์ฌํญ์ด๋ ๋ด๋น์ ์๊ฒฌ์ ์
๋ ฅํฉ๋๋ค.", |
| ), |
| "์ค์น์ผ์": st.column_config.DateColumn( |
| "์ค์น์ผ์", |
| format="YYYY-MM-DD", |
| ), |
| "์ฌ์ฉ์ฐ์": st.column_config.NumberColumn( |
| "์ฌ์ฉ์ฐ์", |
| format="%.1f๋
", |
| ), |
| "๋ฐฐํฐ๋ฆฌ์๋_pct": st.column_config.NumberColumn( |
| "๋ฐฐํฐ๋ฆฌ์๋_pct", |
| format="%.1f%%", |
| ), |
| "์ต๊ทผ์ค๋ฅ๊ฑด์": st.column_config.NumberColumn( |
| "์ต๊ทผ์ค๋ฅ๊ฑด์", |
| format="%d๊ฑด", |
| ), |
| "์ฐธ๊ณ _๊ต์ฒด์ ์": st.column_config.NumberColumn( |
| "์ฐธ๊ณ _๊ต์ฒด์ ์", |
| format="%.0f์ ", |
| ), |
| }, |
| ) |
|
|
| st.download_button( |
| "ํ์ฌ ๊ต์ฒด ํ๋ณด CSV ๋ค์ด๋ก๋", |
| data=to_csv_bytes(candidates), |
| file_name="meter_replacement_candidates.csv", |
| mime="text/csv", |
| disabled=candidates.empty, |
| ) |
|
|
| with selected_tab: |
| st.subheader("์ ํ ๊ฒฐ๊ณผ") |
|
|
| if "์ ํ" in edited_candidates.columns: |
| selected_rows = edited_candidates[edited_candidates["์ ํ"] == True].copy() |
| else: |
| selected_rows = pd.DataFrame() |
|
|
| selected_count = int(len(selected_rows)) |
| progress_rate = min(selected_count / daily_target, 1.0) |
|
|
| st.progress( |
| progress_rate, |
| text=f"์ค๋ ์ฒ๋ฆฌ ๋ชฉํ {daily_target}๊ฑด ์ค {selected_count}๊ฑด ์ ํ", |
| ) |
|
|
| if selected_rows.empty: |
| st.info("์์ง ์ ํํ ๊ต์ฒด ํ๋ณด๊ฐ ์์ต๋๋ค. `๊ณ๋๊ธฐ ๊ต์ฒด ํ๋ณด` ํญ์์ ํ๋ณด๋ฅผ ์ ํํด ์ฃผ์ธ์.") |
| else: |
| st.dataframe( |
| selected_rows, |
| use_container_width=True, |
| hide_index=True, |
| ) |
|
|
| st.download_button( |
| "์ ํ ๊ฒฐ๊ณผ CSV ๋ค์ด๋ก๋", |
| data=to_csv_bytes(selected_rows), |
| file_name="selected_meter_replacement.csv", |
| mime="text/csv", |
| disabled=selected_rows.empty, |
| ) |
|
|
| with guide_tab: |
| st.subheader("๋ฐ์ดํฐ ์ค๋ช
") |
|
|
| st.markdown("#### ์ฃผ์ ์ปฌ๋ผ ์ค๋ช
") |
|
|
| column_description = pd.DataFrame( |
| [ |
| ["๊ณ๋๊ธฐID", "๊ต์ฒด ๋์ ๊ณ๋๊ธฐ๋ฅผ ๊ตฌ๋ถํ๋ ID", "ํ์ฅ ํ์ธ ๋ฐ ๊ฒฐ๊ณผ ๊ด๋ฆฌ"], |
| ["๊ณ ๊ฐID", "๊ณ ๊ฐ์ ๊ตฌ๋ถํ๋ ID", "๊ณ ๊ฐ ๋จ์ ๊ฒํ "], |
| ["์๋น์ค์ผํฐ", "๋ด๋น ์๋น์ค์ผํฐ", "์ผํฐ๋ณ ํ๋ณด ํํฐ๋ง"], |
| ["์ง์ญ", "๊ณ ๊ฐ ๋๋ ๊ณ๋๊ธฐ๊ฐ ์ํ ์ง์ญ", "์ง์ญ๋ณ ํ๋ณด ํ์ธ"], |
| ["์ค์น์ผ์", "๊ณ๋๊ธฐ๊ฐ ์ค์น๋ ๋ ์ง", "๋
ธํ๋ ํ๋จ"], |
| ["์ฌ์ฉ์ฐ์", "๊ณ๋๊ธฐ ์ฌ์ฉ ๊ธฐ๊ฐ", "์ฅ๊ธฐ ์ฌ์ฉ ์ฌ๋ถ ํ๋จ"], |
| ["๋ฐฐํฐ๋ฆฌ์๋_pct", "๊ณ๋๊ธฐ ๋ฐฐํฐ๋ฆฌ ์๋", "๋ฐฐํฐ๋ฆฌ ๋ถ์กฑ ๋์ ์ ๋ณ"], |
| ["์ต๊ทผ์ค๋ฅ๊ฑด์", "์ต๊ทผ ์ค๋ฅ ๋ฐ์ ๊ฑด์", "์ด์ ์งํ ํ๋จ"], |
| ["ํต์ ์ํ", "์ต๊ทผ ์๊ฒฉ ๊ฒ์นจ ๋๋ ํต์ ์ํ", "ํต์ ์ด์ ์ฌ๋ถ ํ์ธ"], |
| ["๋ถํ์ฌ๊ณ ์ฌ๋ถ", "๊ต์ฒด ๋ถํ ์ฌ๊ณ ๋ณด์ ์ฌ๋ถ", "์ฆ์ ์ฒ๋ฆฌ ๊ฐ๋ฅ ์ฌ๋ถ ํ๋จ"], |
| ["์ฐธ๊ณ _๊ต์ฒด์ ์", "์๋ ์ฐ์ ๋ ๊ต์ฒด ์ฐ์ ์์ ์ ์", "ํ๋ณด ์ถ์ถ ๊ธฐ์ค"], |
| ["์ฐธ๊ณ _๊ต์ฒด๋ฑ๊ธ", "๊ต์ฒด ์ถ์ฒ ๋ฑ๊ธ", "์ฆ์๊ต์ฒด ํ๋ณด ๊ตฌ๋ถ"], |
| ["๊ต์ฒด์ฌ์ ", "๊ต์ฒด ํ๋ณด๋ก ์ถ์ฒ๋ ์ฌ์ ", "๋ด๋น์ ๊ฒํ ์ฐธ๊ณ "], |
| ["๊ถ์ฅ์กฐ์น", "๊ถ์ฅ๋๋ ์กฐ์น ๋ด์ฉ", "์ต์ข
์กฐ์น ํ๋จ"], |
| ], |
| columns=["์ปฌ๋ผ๋ช
", "์ค๋ช
", "ํ์ฉ ๋ฐฉ์"], |
| ) |
|
|
| st.dataframe( |
| column_description, |
| use_container_width=True, |
| hide_index=True, |
| ) |
|
|
| st.markdown("#### ๊ธฐ์ค๊ฐ ์กฐ์ ์ ๋ฐ๋ฅธ ๋ณํ") |
| st.write("- ๊ต์ฒด ์ต์ ์ ์๋ฅผ ๋์ด๋ฉด ๊ต์ฒด ํ๋ณด ์๋ ์ค๊ณ , ํ๊ท ๊ต์ฒด ์ ์๋ ๋์์ง ์ ์์ต๋๋ค.") |
| st.write("- ๋ฐฐํฐ๋ฆฌ ์๋ ๊ธฐ์ค์ ๋ฎ์ถ๋ฉด ๋ฐฐํฐ๋ฆฌ๊ฐ ๋ ๋ถ์กฑํ ๊ณ๋๊ธฐ ์ค์ฌ์ผ๋ก ํ๋ณด๊ฐ ์ค์ด๋ญ๋๋ค.") |
| st.write("- ๋ถํ ์ฌ๊ณ ๊ฐ ์๋ ๊ฑด๋ง ๋ณด๊ธฐ ์ต์
์ ์ผ๋ฉด ์ฆ์ ์ฒ๋ฆฌ ๊ฐ๋ฅํ ํ๋ณด ์ค์ฌ์ผ๋ก ๋ชฉ๋ก์ด ์ค์ด๋ญ๋๋ค.") |
| st.write("- ์๋น์ค์ผํฐ, ๊ต์ฒด๋ฑ๊ธ, ํต์ ์ํ๋ฅผ ์ ํํ๋ฉด ํด๋น ์กฐ๊ฑด์ ๋ง๋ ํ๋ณด๋ง ํ์๋ฉ๋๋ค.") |
|
|
| show_raw_data = st.toggle("์๋ณธ ๋ฐ์ดํฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ", value=False) |
|
|
| if show_raw_data: |
| st.dataframe( |
| meter.head(50), |
| use_container_width=True, |
| hide_index=True, |
| ) |