카테고리 없음

Streamlit 대시보드 작성 4일차

iron-min 2025. 12. 1. 16:12

1. 오늘 배운것 : 대시보드 구성

 

 

 

 

1-1. 메인 앱 만들기

→ 페이지 라우팅, 전체 레이아웃 설정, 페이지 간 전환을 담당

 

import streamlit as st

# 앱 전체 페이지 타이틀, 레이아웃 설정
st.set_page_config(page_title="제조 결함 대시보드", layout="wide")

# 페이지 리스트 정의
pages = [
    st.Page("dashboard_page.py", title="대시보드", icon="📊", default=True),
    st.Page("prediction_page.py", title="예측 및 EDA", icon="🤖"),
]

# 페이지 네비게이션 생성
pg = st.navigation(pages)

# 사용자가 선택한 페이지 실행
pg.run()

 

페이지 설정

st.set_page_config()  : 앱의 제목, 레이아웃 등 설정

 

페이지 목록 정의

st.Page() : 어떤 파일이 어떤 이름으로 푯시될지 결정

 

네비게이션&실행

pg = st.navigation(pages)

pg.run()

 

사용자가 사이드바나 상단 탭 등에서 페이지를 선택하면, 해당 페이지에 정의된 함수 실행

 

 

 

1-2. KPI 대시보드 만들기

 

① 패키지 로드

streamlit : 웹 UI 구성

pandas : 데이터 프레임 처리

plotly.express / plotly.graph_objects : 시각화 패키지

 

import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

 

 

② CSV 데이터 로드 함수 : load_data()

csv파일 (modified_manufacturing_dataset.csv)에서 데이터를 불러오고, 날짜 date 컬럼을 datetime으로 변환

@st.cache_data를 통해 캐싱하여 재로드 방지

@st.cache_data(show_spinner=False)
def load_data(csv_path="modified_manufacturing_dataset.csv"):
    """
    CSV 파일을 불러온 뒤 'Date' 컬럼을 datetime 타입으로 파싱합니다.
    'st.cache_data' 데코레이터로 감싸 데이터 재로드를 방지(캐싱)합니다.
    """
    df = pd.read_csv(csv_path, parse_dates=["Date"])
    return df

@st.cache_data : streamlit의 데이터 캐싱 데코레이터, 동일한 csv경로를 재호출 해도, 메모리에 저장된 데이터를 재사용해 속도를 높임.

parse_dates=['Date'] : CSV의 DATE컬럼을 문자열 대신 datetime 형태로 자동 변환, 이후 시계열 연산에 사용

 

③ 메인 함수

### Main 공간 ###

st.image("main_image.png")

df = load_data()

# 각 연/월 컬럼 및 월별 평균 계산산
df["YearMonth"] = (
    df["Date"].dt.year.astype(str) + "년 " + df["Date"].dt.month.astype(str) + "월"
)
df["MonthStart"] = df["Date"].dt.to_period("M").dt.to_timestamp()

monthly_avg = (
    df.groupby("MonthStart")
    .agg(
        {
            "ProductionVolume": "mean",
            "QualityScore": "mean",
            "WorkerProductivity": "mean",
        }
    )
    .reset_index()
)

 

데이터프레임 전처리:

YearMonth(연/월) 생성 → 특정기간(월) 선택에 활용

MonthStart(각 달의 첫날) → 월별 평균(monthly_avg) 집계 시 사용

 

④ 사이드바 기간선택 & 현재/이전 달 데이터 구분

# 사이드바에서 기간(연월) 선택
available_yearmonths = df.sort_values("Date")["YearMonth"].unique()

with st.sidebar:
    selected_yearmonth = st.selectbox("기간 선택", available_yearmonths)


# 문자열 변환
parts = selected_yearmonth.split("년 ")
selected_year = int(parts[0])
selected_month = int(parts[1].replace("월", ""))
selected_month_start = pd.Timestamp(year=selected_year, month=selected_month, day=1)

# 현재 기간 데이터
df_selected = df[df["YearMonth"] == selected_yearmonth].copy()
df_selected["DayOfWeek"] = df_selected["Date"].dt.day_name()

# 이전 기간 데이터(전월) 가져오기
selected_index = list(available_yearmonths).index(selected_yearmonth)
if selected_index > 0:
    previous_yearmonth = list(available_yearmonths)[selected_index - 1]
    df_prev = df[df["YearMonth"] == previous_yearmonth]
else:
    previous_yearmonth = None
    df_prev = None

 

사이드 바 설정

st.selectbox() : '연도+월' 리스트에서 하나를 고르게 함.

선택기간

selected_yearmonth를 parts = ['2023','5월'] 식으로 분할 → datetime 객체 selected_month_start를 만듬

 

df_selected : 현재 달의 데이터만 추출

df_prev : 바로 이전 달 데이터(전월) 없으면 None.

 

 

⑤ 결함률 KPI

# =============================================================================
# KPI 1: 결함률 (DefectStatus)
# =============================================================================
with st.expander("", expanded=True):
    st.header(f"결함률 ({selected_yearmonth})")

    # 현재 달 결함률 vs 전체 평균 vs 이전 달
    current_defect_rate = df_selected["DefectStatus"].mean() * 100
    overall_defect_rate = df["DefectStatus"].mean() * 100

    if df_prev is not None:
        previous_defect_rate = df_prev["DefectStatus"].mean() * 100
        delta_defect = current_defect_rate - previous_defect_rate
    else:
        delta_defect = 0

    col_defect1, col_defect2 = st.columns(2)
    with col_defect1:
        # st.metric: 숫자 + 증감 표시
        st.metric(
            label="결함률",
            value=f"{current_defect_rate:.1f}%",
            delta=f"{delta_defect:+.1f}%",
        )
    with col_defect2:
        # 전체 평균 대비
        if current_defect_rate <= overall_defect_rate:
            st.info("결함률이 전체 평균 이하로 양호한 상태입니다.")
        else:
            st.error("결함률이 전체 평균 이상입니다. 개선이 필요합니다.")

 

결함률 : DefectStatus(0/1) 평균 X 100 → 퍼센트로 변환

st.metric : 현재값 + 전원 대비 증감 표시

st.info/st.error : 전체 평균보다 낮으면 info, 높으면 error 메세지 표출

 

 

⑥ 생산량 KPI / 품질점수 KPI / 작업자 생산성 KPI

 

▶ 생산량

col1, col2, col3 = st.columns(3)
# =============================================================================
# KPI 2: 생산량 (ProductionVolume)
# =============================================================================
with col1:
    with st.expander("", expanded=True):
        st.subheader(f"{selected_yearmonth} 생산량 KPI")

        # 전체 평균(목표치), 현재 평균, 이전 평균
        overall_target_pv = df["ProductionVolume"].mean()
        current_avg_pv = df_selected["ProductionVolume"].mean()
        if df_prev is not None:
            previous_avg_pv = df_prev["ProductionVolume"].mean()
        else:
            previous_avg_pv = None

        # 생산량 게이지 차트
        gauge_fig_pv = go.Figure(
            go.Indicator(
                mode="gauge+number+delta",
                value=current_avg_pv,
                delta=(
                    {"reference": previous_avg_pv}
                    if previous_avg_pv is not None
                    else {}
                ),
                gauge={
                    "axis": {"range": [0, overall_target_pv * 2]},
                    "bar": {"color": "blue"},
                    "steps": [
                        {"range": [0, overall_target_pv], "color": "lightgray"},
                        {
                            "range": [overall_target_pv, overall_target_pv * 2],
                            "color": "gray",
                        },
                    ],
                    "threshold": {
                        "line": {"color": "red", "width": 4},
                        "thickness": 0.75,
                        "value": overall_target_pv,
                    },
                },
                title={"text": f"목표: {overall_target_pv:.1f} units"},
            )
        )
        st.plotly_chart(gauge_fig_pv, use_container_width=True)

        # 이전 기간 대비 증감(metric)
        if previous_avg_pv is not None:
            delta_value_pv = current_avg_pv - previous_avg_pv
            arrow = "⬆️" if delta_value_pv >= 0 else "⬇️"
            st.metric(
                "전 기간 대비 변화",
                f"{current_avg_pv:.2f} units",
                f"{arrow} {delta_value_pv:+.2f} units",
            )
        else:
            st.write("이전 기간 데이터가 없습니다.")

        # (A) 월별 생산량 평균 라인 차트
        fig_line_pv = px.line(
            monthly_avg,
            x="MonthStart",
            y="ProductionVolume",
            title="월별 생산량 평균",
        )
        fig_line_pv.add_vline(
            x=selected_month_start, line_dash="dash", line_color="red"
        )
        st.plotly_chart(fig_line_pv, use_container_width=True)

        # (B) 생산량 분포 히스토그램
        hist_fig_pv = px.histogram(
            df_selected,
            x="ProductionVolume",
            nbins=20,
            title=f"{selected_yearmonth} 생산량 분포",
        )
        st.plotly_chart(hist_fig_pv, use_container_width=True)

        # (C) 생산량 vs 작업자 생산성 산점도
        scatter_fig_pv = px.scatter(
            df_selected,
            x="ProductionVolume",
            y="WorkerProductivity",
            color="QualityScore",
            title=f"{selected_yearmonth} 생산량 vs 작업자 생산성",
        )
        st.plotly_chart(scatter_fig_pv, use_container_width=True)

        # (D) 요일별 생산량 분포 박스 플롯
        box_fig_pv = px.box(
            df_selected,
            x="DayOfWeek",
            y="ProductionVolume",
            category_orders={
                "DayOfWeek": [
                    "Monday",
                    "Tuesday",
                    "Wednesday",
                    "Thursday",
                    "Friday",
                    "Saturday",
                    "Sunday",
                ]
            },
            title=f"{selected_yearmonth} 요일별 생산량 분포",
        )
        st.plotly_chart(box_fig_pv, use_container_width=True)

 

게이지 차트 : Plotly의 go.Indicator로 목표 vs 현재값 + 전월 대비 증감을 한 눈에 표시

 

1) 차트 생성

go.Figure(go.Indicator(...))

 - go indicator는 게이지 차트 또는 숫자 표시 위젯을 만드는 Plotly 객체

 - mode = 'gauge+number+delta'

   → gauge : 원형게이지 표시

   → number : 현재값을 수치로 표시

   → delta : 이전 값과의 차이를 표시

 - value=current_avg_pv : 현재 평균 PV 값을 게이지 차트의 값으로 설정

gauge_fig_pv = go.Figure(
    go.Indicator(
        mode="gauge+number+delta",
        value=current_avg_pv,

 

2) delta(변화량) 설정

delta=(
    {"reference": previous_avg_pv}
    if previous_avg_pv is not None
    else {}
),

delta : 이전 평균 PV 값과 비교하여 변화량을 표시

previous_avg_pv 값이 존재할 경우 {'reference':previous_avg_pv}를 설정

previous_avg_pv 가 None 이면 {}

 

3) 게이지 설정

gauge={
   "axis": {"range": [0, overall_target_pv * 2]},
   "bar": {"color": "blue"},

axis : 게이지의 범위 설정

 - 최소값 : 0

 - 최대값 : overall_target_pv*2 (목표의 2배까지)

bar : 현재 값을 나타내는 bar의 색상을 파란색으로 설정

 

4) 색상 단계(steps)

"steps": [
    {"range": [0, overall_target_pv], "color": "lightgray"},
    {
        "range": [overall_target_pv, overall_target_pv * 2],
        "color": "gray",
    },
],

steps : 색상 구간을 설정

 - [0, overall_target_pv] : 연한 회색(lightgray)

 - [overall_target_pv, overall_target_pv*2] : 회색(gray)

 

5) 임계값(threshold) 설정

"threshold": {
    "line": {"color": "red", "width": 4},
    "thickness": 0.75,
    "value": overall_target_pv,
},

threshold : 임계값을 시각적으로 강조

 - 빨간색 선(color:red) 로 표시

 - 선의 두께 :4

 - thickness : 게이지의 두께

 - value=overall_target_pv : 임계값은 목표 pv 값으로 설정

 

6) 차트 제목 설정

title={"text": f"목표: {overall_target_pv:.1f} units"},

 

 

 

라인 차트 : px.line 으로 월별 평균 추이 확인

 

히스토그램 : 현재 달 (df_selected) 에서 생산량 분포

산점도 : 생산량 vs 작업자 생산성, color= 'QualityScore'로 점 색상 구분

 

박스플롯 : 요일별 생산량 차이 파악

▶ 품질점수

with col2:
    with st.expander("", expanded=True):
        st.subheader(f"{selected_yearmonth} 품질 점수 KPI")

        # 전체 평균(목표치), 현재 평균, 이전 평균
        overall_target_qs = df["QualityScore"].mean()
        current_avg_qs = df_selected["QualityScore"].mean()
        if df_prev is not None:
            previous_avg_qs = df_prev["QualityScore"].mean()
        else:
            previous_avg_qs = None

        # 품질 점수 게이지 차트
        gauge_fig_qs = go.Figure(
            go.Indicator(
                mode="gauge+number+delta",
                value=current_avg_qs,
                delta=(
                    {"reference": previous_avg_qs}
                    if previous_avg_qs is not None
                    else {}
                ),
                gauge={
                    "axis": {"range": [0, overall_target_qs * 1.5]},
                    "bar": {"color": "green"},
                    "steps": [
                        {"range": [0, overall_target_qs], "color": "lightgray"},
                        {
                            "range": [overall_target_qs, overall_target_qs * 1.5],
                            "color": "gray",
                        },
                    ],
                    "threshold": {
                        "line": {"color": "red", "width": 4},
                        "thickness": 0.75,
                        "value": overall_target_qs,
                    },
                },
                title={"text": f"목표: {overall_target_qs:.1f}%"},
            )
        )
        st.plotly_chart(gauge_fig_qs, use_container_width=True)

        # 이전 기간 대비 증감
        if previous_avg_qs is not None:
            delta_value_qs = current_avg_qs - previous_avg_qs
            arrow = "⬆️" if delta_value_qs >= 0 else "⬇️"
            st.metric(
                "전 기간 대비 변화",
                f"{current_avg_qs:.1f}%",
                f"{arrow} {delta_value_qs:+.2f}%",
            )
        else:
            st.write("이전 기간 데이터가 없습니다.")

        # (A) 월별 품질 점수 평균 라인 차트
        fig_line_qs = px.line(
            monthly_avg,
            x="MonthStart",
            y="QualityScore",
            title="월별 품질 점수 평균",
        )
        fig_line_qs.add_vline(
            x=selected_month_start, line_dash="dash", line_color="red"
        )
        st.plotly_chart(fig_line_qs, use_container_width=True)

        # (B) 품질 점수 분포 히스토그램
        hist_fig_qs = px.histogram(
            df_selected,
            x="QualityScore",
            nbins=20,
            title=f"{selected_yearmonth} 품질 점수 분포",
        )
        st.plotly_chart(hist_fig_qs, use_container_width=True)

        # (C) 품질 점수 vs 생산량 산점도
        scatter_fig_qs = px.scatter(
            df_selected,
            x="QualityScore",
            y="ProductionVolume",
            color="WorkerProductivity",
            title=f"{selected_yearmonth} 품질 점수 vs 생산량",
        )
        st.plotly_chart(scatter_fig_qs, use_container_width=True)

        # (D) 요일별 품질 점수 분포 박스 플롯
        box_fig_qs = px.box(
            df_selected,
            x="DayOfWeek",
            y="QualityScore",
            category_orders={
                "DayOfWeek": [
                    "Monday",
                    "Tuesday",
                    "Wednesday",
                    "Thursday",
                    "Friday",
                    "Saturday",
                    "Sunday",
                ]
            },
            title=f"{selected_yearmonth} 요일별 품질 점수 분포",
        )
        st.plotly_chart(box_fig_qs, use_container_width=True)

 

▶ 생산성

# =============================================================================
# KPI 4: 작업자 생산성 (WorkerProductivity)
# =============================================================================
with col3:
    with st.expander("", expanded=True):
        st.subheader(f"{selected_yearmonth} 작업자 생산성 KPI")

        # 전체 평균(목표치), 현재 평균, 이전 평균
        overall_target_wp = df["WorkerProductivity"].mean()
        current_avg_wp = df_selected["WorkerProductivity"].mean()
        if df_prev is not None:
            previous_avg_wp = df_prev["WorkerProductivity"].mean()
        else:
            previous_avg_wp = None

        # 작업자 생산성 게이지 차트
        gauge_fig_wp = go.Figure(
            go.Indicator(
                mode="gauge+number+delta",
                value=current_avg_wp,
                delta=(
                    {"reference": previous_avg_wp}
                    if previous_avg_wp is not None
                    else {}
                ),
                gauge={
                    "axis": {"range": [0, overall_target_wp * 1.5]},
                    "bar": {"color": "orange"},
                    "steps": [
                        {"range": [0, overall_target_wp], "color": "lightgray"},
                        {
                            "range": [overall_target_wp, overall_target_wp * 1.5],
                            "color": "gray",
                        },
                    ],
                    "threshold": {
                        "line": {"color": "red", "width": 4},
                        "thickness": 0.75,
                        "value": overall_target_wp,
                    },
                },
                title={"text": f"목표: {overall_target_wp:.1f}%"},
            )
        )
        st.plotly_chart(gauge_fig_wp, use_container_width=True)

        # 이전 기간 대비 증감
        if previous_avg_wp is not None:
            delta_value_wp = current_avg_wp - previous_avg_wp
            arrow = "⬆️" if delta_value_wp >= 0 else "⬇️"
            st.metric(
                "전 기간 대비 변화",
                f"{current_avg_wp:.1f}%",
                f"{arrow} {delta_value_wp:+.2f}%",
            )
        else:
            st.write("이전 기간 데이터가 없습니다.")

        # (A) 월별 작업자 생산성 평균 라인 차트
        fig_line_wp = px.line(
            monthly_avg,
            x="MonthStart",
            y="WorkerProductivity",
            title="월별 작업자 생산성 평균",
        )
        fig_line_wp.add_vline(
            x=selected_month_start, line_dash="dash", line_color="red"
        )
        st.plotly_chart(fig_line_wp, use_container_width=True)

        # (B) 작업자 생산성 분포 히스토그램
        hist_fig_wp = px.histogram(
            df_selected,
            x="WorkerProductivity",
            nbins=20,
            title=f"{selected_yearmonth} 작업자 생산성 분포",
        )
        st.plotly_chart(hist_fig_wp, use_container_width=True)

        # (C) 작업자 생산성 vs 품질 점수 산점도
        scatter_fig_wp = px.scatter(
            df_selected,
            x="WorkerProductivity",
            y="QualityScore",
            color="ProductionVolume",
            title=f"{selected_yearmonth} 작업자 생산성 vs 품질 점수",
        )
        st.plotly_chart(scatter_fig_wp, use_container_width=True)

        # (D) 요일별 작업자 생산성 분포 박스 플롯
        box_fig_wp = px.box(
            df_selected,
            x="DayOfWeek",
            y="WorkerProductivity",
            category_orders={
                "DayOfWeek": [
                    "Monday",
                    "Tuesday",
                    "Wednesday",
                    "Thursday",
                    "Friday",
                    "Saturday",
                    "Sunday",
                ]
            },
            title=f"{selected_yearmonth} 요일별 작업자 생산성 분포",
        )
        st.plotly_chart(box_fig_wp, use_container_width=True)

 

1-3. 예측 대시보드 만들기(prediction_page.py)

① 페이지 로드

- streamlit, pandas, numpy, plotly.express, pickle

import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import pickle

 

② 데이터/모델 로드

@st.cache_data(show_spinner=False)
def load_data(csv_path="modified_manufacturing_dataset.csv"):
    """
    CSV 파일을 불러오고, 캐싱하여 반환.
    """
    df = pd.read_csv(csv_path)
    return df

@st.cache_data(show_spinner=False)
def load_model(model_path="rf_top4_model.pkl"):
    """
    pickle 형태로 저장된 학습 모델을 불러와 캐싱합니다.
    """
    with open(model_path, "rb") as f:
        model = pickle.load(f)
    return model

 

load_data & load_model 함수

 - CSV 파일 (EDA용) 과 사전 학습된 모델(.pkl) 로드

 - @st.cache_data, @st,cache_resource로 캐싱

### Main 공간 ###
st.title("머신러닝 예측 및 EDA")

df = load_data()
model = load_model()

load_data: 캐싱된 CSV → 원본 데이터셋(df)

load_model: 캐싱된 사전 학습 모델 → 예측 시 사용

 

③ 컬럼별 분포 범위 파악

 

# 각 컬럼별 기본 통계값(최소, 최대, 중앙값) 추출 함수
def get_range_info(dataframe, col):
    return {
        "min": float(dataframe[col].min()),
        "max": float(dataframe[col].max()),
        "median": float(dataframe[col].median()),
    }

stockout_info = get_range_info(df, "StockoutRate")
cost_info = get_range_info(df, "ProductionCost")
process_time_info = get_range_info(df, "AdditiveProcessTime")
volume_info = get_range_info(df, "ProductionVolume")

 

get_range_info = 특정 컬럼에 대해 .min(), .max(), median() 추출 → 슬라이더/number_input 을 설정할 때 사용

stockout_info, cost_info, etc → 사용자가 입력할 때 유효범위를 안내.

 

④ 사용자 입력(Form)

# 예측에 필요한 입력 받기
st.header("모델 기반 결함 예측")
with st.form("prediction_form"):
    st.write("예측에 필요한 입력 데이터를 작성하세요.")

    stockout_rate = st.number_input(
        "재고 품절률 (StockoutRate, %)",
        min_value=round(stockout_info["min"], 2),
        max_value=round(stockout_info["max"], 2),
        value=round(stockout_info["median"], 2),
        step=0.1,
    )

    production_cost = st.slider(
        "제조 비용 (ProductionCost, $)",
        min_value=round(cost_info["min"], 2),
        max_value=round(cost_info["max"], 2),
        value=round(cost_info["median"], 2),
        step=1.0,
    )

    additive_process_time = st.slider(
        "적층 공정 시간 (AdditiveProcessTime, hours)",
        min_value=round(process_time_info["min"], 2),
        max_value=round(process_time_info["max"], 2),
        value=round(process_time_info["median"], 2),
        step=0.5,
    )

    production_volume = st.slider(
        "생산량 (ProductionVolume)",
        min_value=int(volume_info["min"]),
        max_value=int(volume_info["max"]),
        value=int(volume_info["median"]),
        step=50,
    )

    submitted = st.form_submit_button("예측 실행")

 

Form 컨테이너 : with st.form() → 여러 입력을 묶어서 한 번에 처리(예측 실행 버튼 클릭 시)

number_input / slider : 최소/최대/기본값을 ㅜ이에서 구한 통계치로 설정

submitted : 폼 제출 여부

 

⑤ 예측 결과 표시

# 예측 결과
if submitted:
    # 입력값
    input_data = {
        "StockoutRate": [stockout_rate],
        "ProductionCost": [production_cost],
        "AdditiveProcessTime": [additive_process_time],
        "ProductionVolume": [production_volume],
    }
    features_df = pd.DataFrame(input_data)

    # 모델 예측 (0: 낮은 결함, 1: 높은 결함)
    prediction = model.predict(features_df)[0]
    probability = model.predict_proba(features_df)[0][prediction]

    if prediction == 0:
        st.success(f"예측 결과: 낮은 결함 발생 (확률: {probability:.2f})")
    else:
        st.error(f"예측 결과: 높은 결함 발생 (확률: {probability:.2f})")

 

폼 제출 시에만 실행 :  if submitted

DateFrame 변환 : features_df가 모델 입력 형태와 동일하도록 구성

model.predict, model.predict_proba 호출 → 결함 여부(0/1) + 확률 반환

결함 예측이 0이면 st.success, 1이면 st.error로 결과 표시

⑥ 입력값 분포 시각화(히스토그램)

    # 입력값 분포 시각화
    st.header("입력값 분포 확인")
    for col, val_list in input_data.items():
        user_val = val_list[0]
        if col in df.columns:
            fig = px.histogram(df, x=col, nbins=20, title=f"{col} 분포")
            fig.add_vline(x=user_val, line_width=3, line_dash="dash", line_color="red")
            st.plotly_chart(fig, use_container_width=True)
        else:
            st.warning(f"데이터셋에 '{col}' 컬럼이 없어 시각화를 표시할 수 없습니다.")

 

px.histogram 으로 원본 데이터 분포 확인.

fig.add_vline() : 사용자 입력값을 빨간 점선으로 표시 → 현재 값이 분포상 어디쯤인지 확인 가능

없는 컬럼이면 st.warning

 

 

 

2. 전체 실습 정리

2-1. 총 생성&저장 한 파일

 

2-2. main.py

import streamlit as st

# 앱 전체 페이지 타이틀, 레이아웃 설정
st.set_page_config(page_title="제조 결함 대시보드", layout="wide")

# 페이지 리스트 정의
pages = [
    st.Page("dashboard_page.py", title="대시보드", icon="📊", default=True),
    st.Page("prediction_page.py", title="예측 및 EDA", icon="🤖"),
]

# 페이지 네비게이션 생성
pg = st.navigation(pages)

# 사용자가 선택한 페이지 실행
pg.run()

 

2-3. dashboard_page.py

import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

@st.cache_data(show_spinner=False)
def load_data(csv_path="modified_manufacturing_dataset.csv"):
    """
    CSV 파일을 불러온 뒤 'Date' 컬럼을 datetime 타입으로 파싱합니다.
    'st.cache_data' 데코레이터로 감싸 데이터 재로드를 방지(캐싱)합니다.
    """
    df = pd.read_csv(r'C:\Users\hoyho\OneDrive\바탕 화면\부트캠프\파이썬\python_folder\Streamlit\streamlit4번째강의\modified_manufacturing_dataset.csv', parse_dates=["Date"])
    return df

### Main 공간 ###

st.image("main_image.png")

df = load_data()

# 각 연/월 컬럼 및 월별 평균 계산산
df["YearMonth"] = (
    df["Date"].dt.year.astype(str) + "년 " + df["Date"].dt.month.astype(str) + "월"
)
df["MonthStart"] = df["Date"].dt.to_period("M").dt.to_timestamp()

monthly_avg = (
    df.groupby("MonthStart")
    .agg(
        {
            "ProductionVolume": "mean",
            "QualityScore": "mean",
            "WorkerProductivity": "mean",
        }
    )
    .reset_index()
)

# 사이드바에서 기간(연월) 선택
available_yearmonths = df.sort_values("Date")["YearMonth"].unique()

with st.sidebar:
    selected_yearmonth = st.selectbox("기간 선택", available_yearmonths)


# 문자열 변환
parts = selected_yearmonth.split("년 ")
selected_year = int(parts[0])
selected_month = int(parts[1].replace("월", ""))
selected_month_start = pd.Timestamp(year=selected_year, month=selected_month, day=1)

# 현재 기간 데이터
df_selected = df[df["YearMonth"] == selected_yearmonth].copy()
df_selected["DayOfWeek"] = df_selected["Date"].dt.day_name()

# 이전 기간 데이터(전월) 가져오기
selected_index = list(available_yearmonths).index(selected_yearmonth)
if selected_index > 0:
    previous_yearmonth = list(available_yearmonths)[selected_index - 1]
    df_prev = df[df["YearMonth"] == previous_yearmonth]
else:
    previous_yearmonth = None
    df_prev = None


# =============================================================================
# KPI 1: 결함률 (DefectStatus)
# =============================================================================
with st.expander("", expanded=True):
    st.header(f"결함률 ({selected_yearmonth})")

    # 현재 달 결함률 vs 전체 평균 vs 이전 달
    current_defect_rate = df_selected["DefectStatus"].mean() * 100
    overall_defect_rate = df["DefectStatus"].mean() * 100

    if df_prev is not None:
        previous_defect_rate = df_prev["DefectStatus"].mean() * 100
        delta_defect = current_defect_rate - previous_defect_rate
    else:
        delta_defect = 0

    col_defect1, col_defect2 = st.columns(2)
    with col_defect1:
        # st.metric: 숫자 + 증감 표시
        st.metric(
            label="결함률",
            value=f"{current_defect_rate:.1f}%",
            delta=f"{delta_defect:+.1f}%",
        )
    with col_defect2:
        # 전체 평균 대비
        if current_defect_rate <= overall_defect_rate:
            st.info("결함률이 전체 평균 이하로 양호한 상태입니다.")
        else:
            st.error("결함률이 전체 평균 이상입니다. 개선이 필요합니다.")

col1, col2, col3 = st.columns(3)
# =============================================================================
# KPI 2: 생산량 (ProductionVolume)
# =============================================================================
with col1:
    with st.expander("", expanded=True):
        st.subheader(f"{selected_yearmonth} 생산량 KPI")

        # 전체 평균(목표치), 현재 평균, 이전 평균
        overall_target_pv = df["ProductionVolume"].mean()
        current_avg_pv = df_selected["ProductionVolume"].mean()
        if df_prev is not None:
            previous_avg_pv = df_prev["ProductionVolume"].mean()
        else:
            previous_avg_pv = None

        # 생산량 게이지 차트
        gauge_fig_pv = go.Figure(
            go.Indicator(
                mode="gauge+number+delta",
                value=current_avg_pv,
                delta=(
                    {"reference": previous_avg_pv}
                    if previous_avg_pv is not None
                    else {}
                ),
                gauge={
                    "axis": {"range": [0, overall_target_pv * 2]},
                    "bar": {"color": "blue"},
                    "steps": [
                        {"range": [0, overall_target_pv], "color": "lightgray"},
                        {
                            "range": [overall_target_pv, overall_target_pv * 2],
                            "color": "gray",
                        },
                    ],
                    "threshold": {
                        "line": {"color": "red", "width": 4},
                        "thickness": 0.75,
                        "value": overall_target_pv,
                    },
                },
                title={"text": f"목표: {overall_target_pv:.1f} units"},
            )
        )
        st.plotly_chart(gauge_fig_pv, use_container_width=True)

        # 이전 기간 대비 증감(metric)
        if previous_avg_pv is not None:
            delta_value_pv = current_avg_pv - previous_avg_pv
            arrow = "⬆️" if delta_value_pv >= 0 else "⬇️"
            st.metric(
                "전 기간 대비 변화",
                f"{current_avg_pv:.2f} units",
                f"{arrow} {delta_value_pv:+.2f} units",
            )
        else:
            st.write("이전 기간 데이터가 없습니다.")

        # (A) 월별 생산량 평균 라인 차트
        fig_line_pv = px.line(
            monthly_avg,
            x="MonthStart",
            y="ProductionVolume",
            title="월별 생산량 평균",
        )
        fig_line_pv.add_vline(
            x=selected_month_start, line_dash="dash", line_color="red"
        )
        st.plotly_chart(fig_line_pv, use_container_width=True)

        # (B) 생산량 분포 히스토그램
        hist_fig_pv = px.histogram(
            df_selected,
            x="ProductionVolume",
            nbins=20,
            title=f"{selected_yearmonth} 생산량 분포",
        )
        st.plotly_chart(hist_fig_pv, use_container_width=True)

        # (C) 생산량 vs 작업자 생산성 산점도
        scatter_fig_pv = px.scatter(
            df_selected,
            x="ProductionVolume",
            y="WorkerProductivity",
            color="QualityScore",
            title=f"{selected_yearmonth} 생산량 vs 작업자 생산성",
        )
        st.plotly_chart(scatter_fig_pv, use_container_width=True)

        # (D) 요일별 생산량 분포 박스 플롯
        box_fig_pv = px.box(
            df_selected,
            x="DayOfWeek",
            y="ProductionVolume",
            category_orders={
                "DayOfWeek": [
                    "Monday",
                    "Tuesday",
                    "Wednesday",
                    "Thursday",
                    "Friday",
                    "Saturday",
                    "Sunday",
                ]
            },
            title=f"{selected_yearmonth} 요일별 생산량 분포",
        )
        st.plotly_chart(box_fig_pv, use_container_width=True)
with col2:
    with st.expander("", expanded=True):
        st.subheader(f"{selected_yearmonth} 품질 점수 KPI")

        # 전체 평균(목표치), 현재 평균, 이전 평균
        overall_target_qs = df["QualityScore"].mean()
        current_avg_qs = df_selected["QualityScore"].mean()
        if df_prev is not None:
            previous_avg_qs = df_prev["QualityScore"].mean()
        else:
            previous_avg_qs = None

        # 품질 점수 게이지 차트
        gauge_fig_qs = go.Figure(
            go.Indicator(
                mode="gauge+number+delta",
                value=current_avg_qs,
                delta=(
                    {"reference": previous_avg_qs}
                    if previous_avg_qs is not None
                    else {}
                ),
                gauge={
                    "axis": {"range": [0, overall_target_qs * 1.5]},
                    "bar": {"color": "green"},
                    "steps": [
                        {"range": [0, overall_target_qs], "color": "lightgray"},
                        {
                            "range": [overall_target_qs, overall_target_qs * 1.5],
                            "color": "gray",
                        },
                    ],
                    "threshold": {
                        "line": {"color": "red", "width": 4},
                        "thickness": 0.75,
                        "value": overall_target_qs,
                    },
                },
                title={"text": f"목표: {overall_target_qs:.1f}%"},
            )
        )
        st.plotly_chart(gauge_fig_qs, use_container_width=True)

        # 이전 기간 대비 증감
        if previous_avg_qs is not None:
            delta_value_qs = current_avg_qs - previous_avg_qs
            arrow = "⬆️" if delta_value_qs >= 0 else "⬇️"
            st.metric(
                "전 기간 대비 변화",
                f"{current_avg_qs:.1f}%",
                f"{arrow} {delta_value_qs:+.2f}%",
            )
        else:
            st.write("이전 기간 데이터가 없습니다.")

        # (A) 월별 품질 점수 평균 라인 차트
        fig_line_qs = px.line(
            monthly_avg,
            x="MonthStart",
            y="QualityScore",
            title="월별 품질 점수 평균",
        )
        fig_line_qs.add_vline(
            x=selected_month_start, line_dash="dash", line_color="red"
        )
        st.plotly_chart(fig_line_qs, use_container_width=True)

        # (B) 품질 점수 분포 히스토그램
        hist_fig_qs = px.histogram(
            df_selected,
            x="QualityScore",
            nbins=20,
            title=f"{selected_yearmonth} 품질 점수 분포",
        )
        st.plotly_chart(hist_fig_qs, use_container_width=True)

        # (C) 품질 점수 vs 생산량 산점도
        scatter_fig_qs = px.scatter(
            df_selected,
            x="QualityScore",
            y="ProductionVolume",
            color="WorkerProductivity",
            title=f"{selected_yearmonth} 품질 점수 vs 생산량",
        )
        st.plotly_chart(scatter_fig_qs, use_container_width=True)

        # (D) 요일별 품질 점수 분포 박스 플롯
        box_fig_qs = px.box(
            df_selected,
            x="DayOfWeek",
            y="QualityScore",
            category_orders={
                "DayOfWeek": [
                    "Monday",
                    "Tuesday",
                    "Wednesday",
                    "Thursday",
                    "Friday",
                    "Saturday",
                    "Sunday",
                ]
            },
            title=f"{selected_yearmonth} 요일별 품질 점수 분포",
        )
        st.plotly_chart(box_fig_qs, use_container_width=True)
        # =============================================================================
# KPI 4: 작업자 생산성 (WorkerProductivity)
# =============================================================================
with col3:
    with st.expander("", expanded=True):
        st.subheader(f"{selected_yearmonth} 작업자 생산성 KPI")

        # 전체 평균(목표치), 현재 평균, 이전 평균
        overall_target_wp = df["WorkerProductivity"].mean()
        current_avg_wp = df_selected["WorkerProductivity"].mean()
        if df_prev is not None:
            previous_avg_wp = df_prev["WorkerProductivity"].mean()
        else:
            previous_avg_wp = None

        # 작업자 생산성 게이지 차트
        gauge_fig_wp = go.Figure(
            go.Indicator(
                mode="gauge+number+delta",
                value=current_avg_wp,
                delta=(
                    {"reference": previous_avg_wp}
                    if previous_avg_wp is not None
                    else {}
                ),
                gauge={
                    "axis": {"range": [0, overall_target_wp * 1.5]},
                    "bar": {"color": "orange"},
                    "steps": [
                        {"range": [0, overall_target_wp], "color": "lightgray"},
                        {
                            "range": [overall_target_wp, overall_target_wp * 1.5],
                            "color": "gray",
                        },
                    ],
                    "threshold": {
                        "line": {"color": "red", "width": 4},
                        "thickness": 0.75,
                        "value": overall_target_wp,
                    },
                },
                title={"text": f"목표: {overall_target_wp:.1f}%"},
            )
        )
        st.plotly_chart(gauge_fig_wp, use_container_width=True)

        # 이전 기간 대비 증감
        if previous_avg_wp is not None:
            delta_value_wp = current_avg_wp - previous_avg_wp
            arrow = "⬆️" if delta_value_wp >= 0 else "⬇️"
            st.metric(
                "전 기간 대비 변화",
                f"{current_avg_wp:.1f}%",
                f"{arrow} {delta_value_wp:+.2f}%",
            )
        else:
            st.write("이전 기간 데이터가 없습니다.")

        # (A) 월별 작업자 생산성 평균 라인 차트
        fig_line_wp = px.line(
            monthly_avg,
            x="MonthStart",
            y="WorkerProductivity",
            title="월별 작업자 생산성 평균",
        )
        fig_line_wp.add_vline(
            x=selected_month_start, line_dash="dash", line_color="red"
        )
        st.plotly_chart(fig_line_wp, use_container_width=True)

        # (B) 작업자 생산성 분포 히스토그램
        hist_fig_wp = px.histogram(
            df_selected,
            x="WorkerProductivity",
            nbins=20,
            title=f"{selected_yearmonth} 작업자 생산성 분포",
        )
        st.plotly_chart(hist_fig_wp, use_container_width=True)

        # (C) 작업자 생산성 vs 품질 점수 산점도
        scatter_fig_wp = px.scatter(
            df_selected,
            x="WorkerProductivity",
            y="QualityScore",
            color="ProductionVolume",
            title=f"{selected_yearmonth} 작업자 생산성 vs 품질 점수",
        )
        st.plotly_chart(scatter_fig_wp, use_container_width=True)

        # (D) 요일별 작업자 생산성 분포 박스 플롯
        box_fig_wp = px.box(
            df_selected,
            x="DayOfWeek",
            y="WorkerProductivity",
            category_orders={
                "DayOfWeek": [
                    "Monday",
                    "Tuesday",
                    "Wednesday",
                    "Thursday",
                    "Friday",
                    "Saturday",
                    "Sunday",
                ]
            },
            title=f"{selected_yearmonth} 요일별 작업자 생산성 분포",
        )
        st.plotly_chart(box_fig_wp, use_container_width=True)

 

2-4. prediction_page.py

import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import pickle

@st.cache_data(show_spinner=False)
def load_data(csv_path=r"C:\Users\hoyho\OneDrive\바탕 화면\부트캠프\파이썬\python_folder\Streamlit\streamlit4번째강의\modified_manufacturing_dataset.csv"):
    """
    CSV 파일을 불러오고, 캐싱하여 반환.
    """
    df = pd.read_csv(csv_path)
    return df

@st.cache_data(show_spinner=False)
def load_model(model_path=r'C:\Users\hoyho\OneDrive\바탕 화면\부트캠프\파이썬\python_folder\Streamlit\streamlit4번째강의\rf_top4_model.pkl'):
    """
    pickle 형태로 저장된 학습 모델을 불러와 캐싱합니다.
    """
    with open(model_path, "rb") as f:
        model = pickle.load(f)
    return model

### Main 공간 ###
st.title("머신러닝 예측 및 EDA")

df = load_data()
model = load_model()

# 각 컬럼별 기본 통계값(최소, 최대, 중앙값) 추출 함수
def get_range_info(dataframe, col):
    return {
        "min": float(dataframe[col].min()),
        "max": float(dataframe[col].max()),
        "median": float(dataframe[col].median()),
    }

stockout_info = get_range_info(df, "StockoutRate")
cost_info = get_range_info(df, "ProductionCost")
process_time_info = get_range_info(df, "AdditiveProcessTime")
volume_info = get_range_info(df, "ProductionVolume")

# 예측에 필요한 입력 받기
st.header("모델 기반 결함 예측")
with st.form("prediction_form"):
    st.write("예측에 필요한 입력 데이터를 작성하세요.")

    stockout_rate = st.number_input(
        "재고 품절률 (StockoutRate, %)",
        min_value=round(stockout_info["min"], 2),
        max_value=round(stockout_info["max"], 2),
        value=round(stockout_info["median"], 2),
        step=0.1,
    )

    production_cost = st.slider(
        "제조 비용 (ProductionCost, $)",
        min_value=round(cost_info["min"], 2),
        max_value=round(cost_info["max"], 2),
        value=round(cost_info["median"], 2),
        step=1.0,
    )

    additive_process_time = st.slider(
        "적층 공정 시간 (AdditiveProcessTime, hours)",
        min_value=round(process_time_info["min"], 2),
        max_value=round(process_time_info["max"], 2),
        value=round(process_time_info["median"], 2),
        step=0.5,
    )

    production_volume = st.slider(
        "생산량 (ProductionVolume)",
        min_value=int(volume_info["min"]),
        max_value=int(volume_info["max"]),
        value=int(volume_info["median"]),
        step=50,
    )

    submitted = st.form_submit_button("예측 실행")

    # 예측 결과
if submitted:
    # 입력값
    input_data = {
        "StockoutRate": [stockout_rate],
        "ProductionCost": [production_cost],
        "AdditiveProcessTime": [additive_process_time],
        "ProductionVolume": [production_volume],
    }
    features_df = pd.DataFrame(input_data)

    # 모델 예측 (0: 낮은 결함, 1: 높은 결함)
    prediction = model.predict(features_df)[0]
    probability = model.predict_proba(features_df)[0][prediction]

    if prediction == 0:
        st.success(f"예측 결과: 낮은 결함 발생 (확률: {probability:.2f})")
    else:
        st.error(f"예측 결과: 높은 결함 발생 (확률: {probability:.2f})")


        st.header("입력값 분포 확인")
    for col, val_list in input_data.items():
        user_val = val_list[0]
        if col in df.columns:
            fig = px.histogram(df, x=col, nbins=20, title=f"{col} 분포")
            fig.add_vline(x=user_val, line_width=3, line_dash="dash", line_color="red")
            st.plotly_chart(fig, use_container_width=True)
        else:
            st.warning(f"데이터셋에 '{col}' 컬럼이 없어 시각화를 표시할 수 없습니다.")

3. 느낀점

아마 실습해보면서 코드 따라가보면 되지 않을까... 생각합니다.

어렵긴한데 튜터님들도 검색하고 찾아가면서 코드 짜시는걸 보면서 한번에 되는건 없구나, 천천히 찾아가면서 하는게 맞구나 라는 생각이 들었습니다.

이번 실습은 1시간이지만 막상 데이터를 받고 이런 코드를 짜려면 굉장히 시간이 많이 들겠다는 생각을 했습니다.