jin0 / src /streamlit_app.py
gaston00's picture
Update src/streamlit_app.py
a1db7b2 verified
Raw
History Blame Contribute Delete
12 kB
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,
)