CoolFace
Apppublic

lscsc/Process_dash_pub

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
quality_dashboard.py1445 linesDownload Raw Back to backup
1# -*- coding: utf-8 -*-
2
3import streamlit as st
4import plotly.graph_objects as go
5import pandas as pd
6import numpy as np
7import warnings
8import platform
9import plotly.express as px
10from scipy import stats
11import streamlit.components.v1 as components
12from datetime import datetime, timedelta
13import os
14
15
16# 페이지 설정
17st.set_page_config(
18    page_title="품질 공정정보 대시보드",
19    page_icon="📊",
20    layout="wide",
21    initial_sidebar_state="expanded"
22)
23
24
25# 경고 메시지 무시
26warnings.filterwarnings('ignore')
27
28
29
30def add_sidebar_styles():
31    """사이드바 스타일 추가 함수 - 간결한 디자인"""
32    st.markdown(
33        """
34        <style>
35        /* 간결한 사이드바 기본 스타일 */
36        [data-testid="stSidebar"] {
37            background-color: #f8f9fa;
38            padding: 1rem 0.5rem;
39            color: #333;
40        }
41        
42        /* 사이드바 내용 컨테이너 */
43        [data-testid="stSidebar"] > div:first-child {
44            padding: 1rem 0.5rem;
45        }
46        
47        /* 사이드바 제목 스타일 */
48        [data-testid="stSidebar"] [data-testid="stMarkdown"] h1 {
49            font-size: 1.3rem;
50            color: #2C3E50;
51            font-weight: 500;
52            margin-bottom: 1rem;
53            padding-bottom: 0.5rem;
54            border-bottom: 1px solid #e0e0e0;
55        }
56        
57        /* 사이드바 소제목 스타일 */
58        [data-testid="stSidebar"] [data-testid="stMarkdown"] h3 {
59            font-size: 1rem;
60            color: #2C3E50;
61            margin-top: 1rem;
62            margin-bottom: 0.5rem;
63            font-weight: 500;
64        }
65        
66        /* 구분선 스타일 */
67        [data-testid="stSidebar"] hr {
68            border-color: #e0e0e0;
69            margin: 1rem 0;
70        }
71        
72        /* 라디오 버튼 컨테이너 스타일 */
73        [data-testid="stSidebar"] [data-testid="stRadio"] > div {
74            display: flex;
75            flex-direction: column;
76            gap: 0.25rem;
77        }
78        
79        /* 라디오 버튼 레이블 스타일 */
80        [data-testid="stSidebar"] [data-testid="stRadio"] > div > label {
81            border-radius: 4px;
82            padding: 0.5rem;
83            transition: all 0.2s ease;
84            border-left: 2px solid transparent;
85        }
86        
87        /* 라디오 버튼 호버 효과 */
88        [data-testid="stSidebar"] [data-testid="stRadio"] > div > label:hover {
89            background-color: rgba(0, 0, 0, 0.05);
90            border-left: 2px solid #3498db;
91        }
92        
93        /* 선택된 라디오 버튼 스타일 */
94        [data-testid="stSidebar"] [data-testid="stRadio"] > div > label[aria-checked="true"] {
95            background-color: rgba(52, 152, 219, 0.1);
96            border-left: 2px solid #3498db;
97        }
98        
99        /* 버튼 스타일 */
100        [data-testid="stSidebar"] button {
101            width: 100%;
102            background-color: #f1f3f5;
103            color: #333;
104            border-radius: 4px;
105            padding: 0.5rem;
106            font-weight: 500;
107            border: 1px solid #dee2e6;
108            margin-top: 1rem;
109            transition: all 0.2s ease;
110        }
111        
112        /* 버튼 호버 효과 */
113        [data-testid="stSidebar"] button:hover {
114            background-color: #e9ecef;
115            border-color: #ced4da;
116        }
117        
118        /* 로고 컨테이너 스타일 */
119        .sidebar-logo-container {
120            text-align: center;
121            padding: 0.5rem 0;
122            margin-bottom: 1rem;
123        }
124        
125        /* 로고 이미지 스타일 */
126        .sidebar-logo-container img {
127            max-width: 60px;
128            height: auto;
129        }
130        
131        /* 사이드바 푸터 스타일 */
132        .sidebar-footer {
133            margin-top: 1rem;
134            padding-top: 0.5rem;
135            border-top: 1px solid #e0e0e0;
136            font-size: 0.75rem;
137            color: #6c757d;
138            text-align: center;
139        }
140        </style>
141        """, 
142        unsafe_allow_html=True
143    )
144
145
146
147# 페이지 설정 및 전체 스타일 개선
148def add_main_styles():
149    """메인 컨텐츠 스타일 추가 함수 - 간결한 디자인"""
150    st.markdown(
151        """
152        <style>
153        /* 전체 페이지 스타일 */
154        .stApp {
155            background-color: #ffffff;
156        }
157        
158        /* 메인 컨테이너 스타일 */
159        [data-testid="stAppViewContainer"] {
160            background-color: #ffffff;
161            padding: 0.5rem;
162        }
163        
164        /* 메인 컨텐츠 영역 스타일 */
165        .main .block-container {
166            padding: 1.5rem;
167            max-width: 1200px;
168            margin: 0 auto;
169        }
170        
171        /* 타이틀 스타일 */
172        h1 {
173            color: #2C3E50;
174            font-weight: 600;
175            font-size: 1.8rem;
176            margin-bottom: 1rem;
177            padding-bottom: 0.5rem;
178            border-bottom: 1px solid #f0f0f0;
179        }
180        
181        /* 서브 타이틀 스타일 */
182        h2, h3 {
183            color: #2C3E50;
184            margin-top: 1rem;
185            margin-bottom: 0.75rem;
186            font-weight: 500;
187        }
188        
189        h3 {
190            font-size: 1.2rem;
191            padding-left: 0.25rem;
192            border-left: 3px solid #3498db;
193        }
194        
195        /* 위젯 간격 조정 */
196        .stSelectbox, .stMultiSelect {
197            margin-bottom: 0.75rem;
198        }
199        
200        /* 메트릭 카드 스타일 */
201        [data-testid="stMetric"] {
202            background-color: #f8f9fa;
203            border-radius: 6px;
204            padding: 0.75rem !important;
205            border-left: 3px solid #3498db;
206        }
207        
208        /* 메트릭 레이블 스타일 */
209        [data-testid="stMetric"] > div:first-child {
210            color: #6c757d;
211        }
212        
213        /* 메트릭 값 스타일 */
214        [data-testid="stMetric"] > div:nth-child(2) {
215            font-size: 1.3rem;
216            font-weight: 600;
217            color: #2C3E50;
218        }
219        
220        /* 데이터프레임 스타일 */
221        .dataframe {
222            border-collapse: collapse !important;
223            width: 100% !important;
224            border-radius: 6px;
225            overflow: hidden;
226        }
227        
228        .dataframe th {
229            background-color: #f1f3f5 !important;
230            color: #495057 !important;
231            font-weight: 500 !important;
232            padding: 0.5rem 0.75rem !important;
233            text-align: left !important;
234        }
235        
236        .dataframe td {
237            padding: 0.5rem 0.75rem !important;
238            border-top: 1px solid #f0f0f0 !important;
239        }
240        
241        .dataframe tr:nth-child(even) {
242            background-color: #f8f9fa !important;
243        }
244        
245        /* 탭 스타일 개선 */
246        .stTabs [data-baseweb="tab-list"] {
247            gap: 0;
248            border-bottom: 1px solid #e0e0e0;
249        }
250        
251        .stTabs [data-baseweb="tab"] {
252            height: 40px;
253            white-space: pre-wrap;
254            background-color: transparent;
255            border-radius: 0;
256            border-bottom: 2px solid transparent;
257            padding: 0 1rem;
258            font-weight: 500;
259            color: #6c757d;
260            transition: all 0.2s ease;
261        }
262        
263        .stTabs [aria-selected="true"] {
264            color: #3498db !important;
265            border-bottom: 2px solid #3498db !important;
266            background-color: transparent !important;
267        }
268        
269        /* 이슈 박스 스타일 */
270        .issue-box {
271            background-color: #f8f9fa;
272            border-radius: 6px;
273            padding: 1rem;
274            margin: 0.75rem 0;
275            border-left: 3px solid #ff7043;
276        }
277        
278        /* 공정 박스 스타일 */
279        .process-box {
280            background-color: #f8f9fa;
281            border-radius: 6px;
282            padding: 1rem;
283            margin: 0.75rem 0;
284            border-left: 3px solid #66bb6a;
285        }
286        
287        /* 불량 박스 스타일 */
288        .defect-box {
289            background-color: #f8f9fa;
290            border-radius: 6px;
291            padding: 1rem;
292            margin: 0.75rem 0;
293            border-left: 3px solid #5c6bc0;
294        }
295        </style>
296        """, 
297        unsafe_allow_html=True
298    )
299
300def check_password():
301    """비밀번호 확인 함수"""
302    if "password_correct" not in st.session_state:
303        st.session_state["password_correct"] = False
304    
305    if not st.session_state["password_correct"]:
306        # 비밀번호 입력 필드를 한 번만 표시
307        password = st.text_input(
308            "비밀번호를 입력하세요", 
309            type="password",
310            key="password_input"
311        )
312        
313        if password:
314            if password == "1234":
315                st.session_state["password_correct"] = True
316                return True
317            else:
318                st.error("비밀번호가 올바르지 않습니다.")
319                return False
320        return False
321    
322    return True
323
324def initialize_session_state():
325    """세션 상태 초기화 함수"""
326    if 'password_correct' not in st.session_state:
327        st.session_state.password_correct = False
328    
329    if 'selected_product' not in st.session_state:
330        st.session_state.selected_product = None
331    
332    if 'selected_data' not in st.session_state:
333        st.session_state.selected_data = None
334        
335    if 'tab_selection' not in st.session_state:
336        st.session_state.tab_selection = "공정 현황"
337    
338    if 'data_loaded' not in st.session_state:
339        st.session_state.data_loaded = False
340    
341    if 'data_dict' not in st.session_state:
342        st.session_state.data_dict = {}
343
344    def password_entered():
345        """사용자가 입력한 비밀번호가 올바른지 확인합니다."""
346        if st.session_state["password"] == "1234":  # 간단한 비밀번호 설정
347            st.session_state["password_correct"] = True
348            del st.session_state["password"]
349        else:
350            st.session_state["password_correct"] = False
351
352    if "password_correct" not in st.session_state:
353        # 로그인 화면 표시
354        st.markdown(
355            """
356            <div class="login-container">
357                <div class="login-logo">
358                    <img src="https://cdn-icons-png.flaticon.com/512/1995/1995515.png" width="80">
359                </div>
360                <h2 class="login-title">품질관리 시스템 로그인</h2>
361            </div>
362            """, 
363            unsafe_allow_html=True
364        )
365        
366        st.text_input(
367            "비밀번호를 입력하세요", 
368            type="password", 
369            on_change=password_entered, 
370            key="password",
371            placeholder="비밀번호 4자리 (힌트: 1234)"
372        )
373        return False
374    elif not st.session_state["password_correct"]:
375        # 잘못된 비밀번호 입력 시
376        st.markdown(
377            """
378            <div class="login-container">
379                <div class="login-logo">
380                    <img src="https://cdn-icons-png.flaticon.com/512/1995/1995515.png" width="80">
381                </div>
382                <h2 class="login-title">품질관리 시스템 로그인</h2>
383                <p class="login-error">😕 비밀번호가 올바르지 않습니다.</p>
384            </div>
385            """, 
386            unsafe_allow_html=True
387        )
388        
389        st.text_input(
390            "비밀번호를 입력하세요", 
391            type="password", 
392            on_change=password_entered, 
393            key="password",
394            placeholder="비밀번호 4자리 (힌트: 1234)"
395        )
396        return False
397    else:
398        return True
399
400def initialize_session_state():
401    """세션 상태 초기화 함수"""
402    if 'selected_product' not in st.session_state:
403        st.session_state.selected_product = None
404    
405    if 'selected_data' not in st.session_state:
406        st.session_state.selected_data = None
407        
408    # 라디오 버튼 선택 상태를 위한 세션 변수
409    if 'tab_selection' not in st.session_state:
410        st.session_state.tab_selection = "공정 현황"
411    
412    # 데이터 로드 상태를 위한 세션 변수
413    if 'data_loaded' not in st.session_state:
414        st.session_state.data_loaded = False
415    
416    # 데이터 저장을 위한 세션 변수
417    if 'data_dict' not in st.session_state:
418        st.session_state.data_dict = {}
419
420def safe_read_excel(filepath, default_data=None):
421    """안전하게 엑셀 파일을 읽는 함수"""
422    try:
423        if os.path.exists(filepath):
424            return pd.read_excel(filepath)
425        else:
426            st.warning(f"파일을 찾을 수 없습니다: {filepath}")
427            return default_data if default_data is not None else pd.DataFrame()
428    except Exception as e:
429        st.warning(f"파일 읽기 오류 ({filepath}): {str(e)}")
430        return default_data if default_data is not None else pd.DataFrame()
431
432def load_data():
433    """데이터 로드 함수"""
434    # 이미 로드된 데이터가 있으면 재사용
435    if st.session_state.data_loaded:
436        return st.session_state.data_dict
437    
438    try:
439        # 데이터 사전 초기화
440        data_dict = {}
441        
442        # 각 데이터 파일 로드 (안전하게)
443        data_dict['process_info'] = safe_read_excel('공정상세정보/장섬유공정정보.xlsx')
444        data_dict['quality_issues'] = safe_read_excel('품질이슈정보/장섬유이슈정보.xlsx')
445        data_dict['process_specs'] = safe_read_excel('제품 및 공정규격/제품 및 공정규격.xlsx')
446        data_dict['process_capability'] = safe_read_excel('월별 공정능력 정보/월별 공정능력 정보.xlsx')
447        data_dict['defect_rates'] = safe_read_excel('불량률 정보/불량률 정보.xlsx')
448        data_dict['customer_info'] = safe_read_excel('거래선 정보/거래선 정보.xlsx')
449        
450        # 날짜 형식 변환 (품질이슈정보의 날짜 컬럼)
451        if not data_dict['quality_issues'].empty and '기준일' in data_dict['quality_issues'].columns:
452            data_dict['quality_issues']['기준일'] = pd.to_datetime(data_dict['quality_issues']['기준일'], errors='coerce')
453        
454        # 데이터 로드 상태 및 데이터 저장
455        st.session_state.data_loaded = True
456        st.session_state.data_dict = data_dict
457        
458        # 데이터 로드 성공 메시지
459        st.success("데이터가 성공적으로 로드되었습니다.")
460        
461        return data_dict
462    except Exception as e:
463        st.error(f"데이터 로드 중 오류 발생: {str(e)}")
464        return {}
465
466def load_process_infographic():
467    """공정 인포그래픽 HTML 로드 함수"""
468    try:
469        filepath = '공정기본정보/유리 장섬유 생산공정 인포그래픽.html'
470        if os.path.exists(filepath):
471            with open(filepath, 'r', encoding='utf-8') as f:
472                html_content = f.read()
473            return html_content
474        else:
475            st.warning(f"인포그래픽 파일을 찾을 수 없습니다: {filepath}")
476            # 대체 HTML 콘텐츠 제공
477            return """
478            <div style="text-align: center; padding: 20px; background-color: #f8f9fa; border-radius: 10px;">
479                <h3>인포그래픽을 불러올 수 없습니다</h3>
480                <p>파일을 찾을 수 없거나 접근할 수 없습니다.</p>
481            </div>
482            """
483    except Exception as e:
484        st.warning(f"인포그래픽 로드 중 오류 발생: {str(e)}")
485        # 오류 발생 시 대체 HTML 콘텐츠 제공
486        return """
487        <div style="text-align: center; padding: 20px; background-color: #f8f9fa; border-radius: 10px;">
488            <h3>인포그래픽 로드 오류</h3>
489            <p>파일을 읽는 중 오류가 발생했습니다.</p>
490        </div>
491        """
492
493def calculate_process_capability(data, ucl, lcl, sigma_level=3):
494    """공정능력지수 계산 함수"""
495    try:
496        if len(data) == 0:
497            return {
498                'Cp': 0,
499                'Cpu': 0,
500                'Cpl': 0,
501                'Cpk': 0,
502                'PPM': 0
503            }
504        
505        mean = data.mean()
506        std = data.std()
507        
508        if std == 0:
509            return {
510                'Cp': float('inf'),
511                'Cpu': float('inf'),
512                'Cpl': float('inf'),
513                'Cpk': float('inf'),
514                'PPM': 0
515            }
516    
517        # 공정능력지수 계산
518        cp = (ucl - lcl) / (6 * std) if std != 0 else float('inf')
519        cpu = (ucl - mean) / (3 * std) if std != 0 else float('inf')
520        cpl = (mean - lcl) / (3 * std) if std != 0 else float('inf')
521        cpk = min(cpu, cpl)
522    
523        # 예상불량률 계산 (ppm 단위)
524        z_upper = (ucl - mean) / std if std != 0 else float('inf')
525        z_lower = (mean - lcl) / std if std != 0 else float('inf')
526        ppm_upper = stats.norm.sf(z_upper) * 1000000
527        ppm_lower = stats.norm.sf(z_lower) * 1000000
528        total_ppm = ppm_upper + ppm_lower
529    
530        return {
531            'Cp': cp,
532            'Cpu': cpu,
533            'Cpl': cpl,
534            'Cpk': cpk,
535            'PPM': total_ppm
536        }
537
538    except Exception as e:
539        st.error(f"공정능력지수 계산 중 오류 발생: {str(e)}")
540        return {
541            'Cp': 0,
542            'Cpu': 0,
543            'Cpl': 0,
544            'Cpk': 0,
545            'PPM': 0
546        }
547
548def display_process_overview(data):
549    """공정 현황 개요 표시 함수"""
550    if not isinstance(data, dict):
551        st.error("유효하지 않은 데이터 형식입니다.")
552        return
553    
554    st.subheader("유리 장섬유 생산공정 개요")
555    
556    # 공정 인포그래픽 표시
557    infographic_html = load_process_infographic()
558    if infographic_html:
559        components.html(infographic_html, height=500, scrolling=True)
560    
561    # 공정 단계별 주요 정보 표시
562    st.markdown("### 공정 단계별 주요 정보")
563    
564    process_info = data.get('process_info', pd.DataFrame())
565    
566    # 공정 정보가 비어 있는 경우 처리
567    if process_info.empty:
568        st.warning("공정 정보를 불러올 수 없습니다.")
569        return
570    
571    # 공정 단계 선택
572    if '표준대공정명' in process_info.columns:
573        process_stages = process_info['표준대공정명'].unique()
574        selected_stage = st.selectbox("공정 단계 선택", process_stages)
575        
576        # 선택된 공정 단계 정보 표시
577        stage_info = process_info[process_info['표준대공정명'] == selected_stage]
578        
579        if not stage_info.empty:
580            stage_info = stage_info.iloc[0]
581            
582            col1, col2 = st.columns(2)
583            
584            with col1:
585                st.markdown(f"""
586                <div class="process-box">
587                    <h4 style="margin-top: 0;">공정 개요</h4>
588                    <p><strong>공정명:</strong> {selected_stage}</p>
589                    <p><strong>주요 작업:</strong> {stage_info.get('주요작업', 'N/A')}</p>
590                    <p><strong>담당부서:</strong> {stage_info.get('담당부서', 'N/A')}</p>
591                </div>
592                """, unsafe_allow_html=True)
593            
594            with col2:
595                st.markdown(f"""
596                <div class="process-box">
597                    <h4 style="margin-top: 0;">품질 관리 포인트</h4>
598                    <p>{stage_info.get('품질 Point (관리점)', 'N/A')}</p>
599                </div>
600                """, unsafe_allow_html=True)
601        else:
602            st.warning(f"{selected_stage} 공정에 대한 정보를 찾을 수 없습니다.")
603    else:
604        st.warning("공정 정보에 '표준대공정명' 컬럼이 없습니다.")
605    
606    # 관련 품질 이슈 표시
607    quality_issues = data.get('quality_issues', pd.DataFrame())
608    
609    # 품질 이슈 정보가 비어 있는 경우 처리
610    if not quality_issues.empty:
611        st.markdown("### 관련 품질 이슈")
612        
613        # 공정 단계와 관련된 품질 이슈 필터링 (공정명 컬럼이 있다고 가정)
614        if '공정명' in quality_issues.columns:
615            related_issues = quality_issues[quality_issues['공정명'].str.contains(selected_stage, na=False)]
616            
617            if not related_issues.empty:
618                # 최근 5개 이슈만 표시
619                recent_issues = related_issues.sort_values('기준일', ascending=False).head(5)
620                
621                for _, issue in recent_issues.iterrows():
622                    st.markdown(f"""
623                    <div class="issue-box">
624                        <h4 style="margin-top: 0;">{issue.get('Q-VOC 번호', 'N/A')} - {issue.get('제목', 'N/A')}</h4>
625                        <p><strong>발생일:</strong> {issue['기준일'].strftime('%Y-%m-%d') if pd.notna(issue.get('기준일')) else 'N/A'}</p>
626                        <p><strong>발생원인:</strong> {issue.get('발생원인(대) 1', 'N/A')} - {issue.get('발생원인(중) 1', 'N/A')} - {issue.get('발생원인(소) 1', 'N/A')}</p>
627                        <p><strong>대책:</strong> {issue.get('대책', 'N/A')}</p>
628                    </div>
629                    """, unsafe_allow_html=True)
630            else:
631                st.info(f"{selected_stage} 공정과 관련된 품질 이슈가 없습니다.")
632        else:
633            st.warning("품질 이슈 정보에 '공정명' 컬럼이 없습니다.")
634    
635    # 공정 규격 정보 표시
636    process_specs = data.get('process_specs', pd.DataFrame())
637    
638    if not process_specs.empty:
639        st.markdown("### 공정 규격 정보")
640        
641        # 공정명으로 필터링 (공정명 컬럼이 있다고 가정)
642        if '공정명' in process_specs.columns:
643            stage_specs = process_specs[process_specs['공정명'].str.contains(selected_stage, na=False)]
644            
645            if not stage_specs.empty:
646                st.dataframe(stage_specs, use_container_width=True)
647            else:
648                st.info(f"{selected_stage} 공정에 대한 규격 정보가 없습니다.")
649        else:
650            st.warning("공정 규격 정보에 '공정명' 컬럼이 없습니다.")
651    else:
652        st.warning("공정 규격 정보를 불러올 수 없습니다.")
653
654def display_quality_analysis(data):
655    """품질 분석 표시 함수"""
656    st.subheader("품질 분석")
657    
658    # 공정능력 정보 확인
659    process_capability = data.get('process_capability', pd.DataFrame())
660    
661    if process_capability.empty:
662        st.warning("공정능력 정보를 불러올 수 없습니다.")
663        return
664    
665    # 제품 선택
666    if '제품' in process_capability.columns:
667        products = process_capability['제품'].unique()
668        if len(products) > 0:
669            selected_product = st.selectbox("제품 선택", products)
670            
671            # 선택된 제품의 공정능력 데이터
672            product_capability = process_capability[process_capability['제품'] == selected_product]
673            
674            # 검사항목 선택
675            if '검사항목' in product_capability.columns and not product_capability.empty:
676                inspection_items = product_capability['검사항목'].unique()
677                if len(inspection_items) > 0:
678                    selected_item = st.selectbox("검사항목 선택", inspection_items)
679                    
680                    # 선택된 검사항목의 데이터
681                    item_data = product_capability[product_capability['검사항목'] == selected_item]
682                    
683                    if not item_data.empty:
684                        # 월별 공정능력 차트
685                        st.markdown("### 월별 공정능력 추이")
686                        
687                        # 필요한 컬럼이 있는지 확인
688                        required_columns = ['월', 'Cpk']
689                        if all(col in item_data.columns for col in required_columns):
690                            fig = go.Figure()
691                            
692                            # Cpk 추이 그래프
693                            fig.add_trace(go.Scatter(
694                                x=item_data['월'],
695                                y=item_data['Cpk'],
696                                mode='lines+markers',
697                                name='Cpk',
698                                line=dict(color='blue', width=2),
699                                marker=dict(size=8)
700                            ))
701                            
702                            # 기준선 (Cpk=1.33)
703                            fig.add_trace(go.Scatter(
704                                x=[item_data['월'].min(), item_data['월'].max()],
705                                y=[1.33, 1.33],
706                                mode='lines',
707                                name='기준 (Cpk=1.33)',
708                                line=dict(color='red', dash='dash')
709                            ))
710                            
711                            # 그래프 레이아웃 설정
712                            fig.update_layout(
713                                title=f"{selected_product} - {selected_item} 공정능력 추이",
714                                xaxis_title="월",
715                                yaxis_title="공정능력지수",
716                                legend=dict(
717                                    orientation="h",
718                                    yanchor="bottom",
719                                    y=1.02,
720                                    xanchor="right",
721                                    x=1
722                                ),
723                                height=400
724                            )
725                            
726                            st.plotly_chart(fig, use_container_width=True)
727                        else:
728                            st.warning("공정능력 추이를 표시하는데 필요한 데이터가 없습니다.")
729                        
730                        # 검사값 분포 차트
731                        st.markdown("### 검사값 분포")
732                        
733                        # 검사값, 규격 상한/하한 데이터
734                        if all(col in item_data.columns for col in ['검사값', '상한', '하한']):
735                            values = item_data['검사값']
736                            if not values.empty:
737                                mean = values.mean()
738                                std = values.std()
739                                usl = item_data['상한'].mean()
740                                lsl = item_data['하한'].mean()
741                                
742                                # 히스토그램 생성
743                                hist_fig = go.Figure()
744                                
745                                # 히스토그램 추가
746                                hist_fig.add_trace(go.Histogram(
747                                    x=values,
748                                    name='검사값 분포',
749                                    opacity=0.7,
750                                    marker=dict(color='royalblue'),
751                                    histnorm='probability density'
752                                ))
753                                
754                                # 정규분포 곡선 추가
755                                x_range = np.linspace(values.min() - 0.5, values.max() + 0.5, 100)
756                                y_range = stats.norm.pdf(x_range, mean, std)
757                                
758                                hist_fig.add_trace(go.Scatter(
759                                    x=x_range,
760                                    y=y_range,
761                                    mode='lines',
762                                    name='정규분포',
763                                    line=dict(color='red', width=2)
764                                ))
765                                
766                                # 규격 상한/하한 추가
767                                hist_fig.add_trace(go.Scatter(
768                                    x=[usl, usl],
769                                    y=[0, max(y_range) * 1.2],
770                                    mode='lines',
771                                    name='상한 규격',
772                                    line=dict(color='green', width=2, dash='dash')
773                                ))
774                                
775                                hist_fig.add_trace(go.Scatter(
776                                    x=[lsl, lsl],
777                                    y=[0, max(y_range) * 1.2],
778                                    mode='lines',
779                                    name='하한 규격',
780                                    line=dict(color='green', width=2, dash='dash')
781                                ))
782                                
783                                # 평균선 추가
784                                hist_fig.add_trace(go.Scatter(
785                                    x=[mean, mean],
786                                    y=[0, max(y_range) * 1.2],
787                                    mode='lines',
788                                    name='평균',
789                                    line=dict(color='black', width=2)
790                                ))
791                                
792                                # 그래프 레이아웃 설정
793                                hist_fig.update_layout(
794                                    title=f"{selected_product} - {selected_item} 검사값 분포",
795                                    xaxis_title="검사값",
796                                    yaxis_title="확률 밀도",
797                                    legend=dict(
798                                        orientation="h",
799                                        yanchor="bottom",
800                                        y=1.02,
801                                        xanchor="right",
802                                        x=1
803                                    ),
804                                    height=400
805                                )
806                                
807                                st.plotly_chart(hist_fig, use_container_width=True)
808                                
809                                # 공정능력 분석 결과
810                                st.markdown("### 공정능력 분석 결과")
811                                
812                                # 공정능력지수 계산
813                                capability = calculate_process_capability(values, usl, lsl)
814                                
815                                col1, col2, col3, col4 = st.columns(4)
816                                
817                                with col1:
818                                    st.metric(label="Cp", value=f"{capability['Cp']:.3f}")
819                                with col2:
820                                    st.metric(label="Cpk", value=f"{capability['Cpk']:.3f}")
821                                with col3:
822                                    st.metric(label="Cpu", value=f"{capability['Cpu']:.3f}")
823                                with col4:
824                                    st.metric(label="Cpl", value=f"{capability['Cpl']:.3f}")
825                                
826                                # 예상 불량률
827                                st.metric(label="예상 불량률 (PPM)", value=f"{capability['PPM']:.2f}")
828                                
829                                # 공정능력 평가
830                                if capability['Cpk'] >= 1.33:
831                                    st.success("공정능력 평가: 우수 (Cpk ≥ 1.33)")
832                                elif capability['Cpk'] >= 1.00:
833                                    st.warning("공정능력 평가: 보통 (1.00 ≤ Cpk < 1.33)")
834                                else:
835                                    st.error("공정능력 평가: 미흡 (Cpk < 1.00)")
836                            else:
837                                st.warning("검사값 데이터가 없습니다.")
838                        else:
839                            st.warning("검사값 분포를 표시하는데 필요한 데이터가 없습니다.")
840                    else:
841                        st.warning(f"{selected_item} 검사항목에 대한 데이터가 없습니다.")
842                else:
843                    st.warning("검사항목이 없습니다.")
844            else:
845                st.warning("공정능력 데이터에 '검사항목' 컬럼이 없거나 데이터가 비어 있습니다.")
846        else:
847            st.warning("제품 정보가 없습니다.")
848    else:
849        st.warning("공정능력 데이터에 '제품' 컬럼이 없습니다.")
850
851def display_defect_analysis(data):
852    """불량 분석 표시 함수"""
853    st.subheader("불량 분석")
854    
855    defect_rates = data.get('defect_rates', pd.DataFrame())
856    
857    if defect_rates.empty:
858        st.warning("불량률 정보를 불러올 수 없습니다.")
859        return
860    
861    # 유형 선택 (공정Loss, 공정불량 등)
862    if '불량구분' in defect_rates.columns:
863        defect_types = defect_rates['불량구분'].unique()
864        if len(defect_types) > 0:
865            selected_defect_type = st.selectbox("불량구분 선택", defect_types)
866            
867            # 선택된 불량구분의 데이터
868            type_defects = defect_rates[defect_rates['불량구분'] == selected_defect_type]
869            
870            if not type_defects.empty:
871                # 세부불량 항목별 분석
872                st.markdown("### 세부불량 항목별 분석")
873                
874                # 연도 선택
875                years = [col for col in type_defects.columns if col.endswith('년') and not col.startswith('5개년')]
876                if years:
877                    selected_year = st.selectbox("연도 선택", years, index=len(years)-1)  # 기본값은 가장 최근 연도
878                    
879                    # 선택된 연도의 세부불량 항목별 데이터 준비
880                    if selected_year in type_defects.columns:
881                        # 세부불량 항목별로 데이터 집계
882                        defect_by_item = type_defects.groupby('세부불량')[selected_year].sum().reset_index()
883                        defect_by_item = defect_by_item.sort_values(selected_year, ascending=False)
884                        
885                        if not defect_by_item.empty:
886                            # 파레토 차트 생성
887                            fig = go.Figure()
888                            
889                            # 불량량 막대 그래프
890                            fig.add_trace(go.Bar(
891                                x=defect_by_item['세부불량'],
892                                y=defect_by_item[selected_year],
893                                name='불량량',
894                                marker=dict(color='indianred')
895                            ))
896                            
897                            # 누적 불량량 계산
898                            defect_by_item['누적비율'] = defect_by_item[selected_year].cumsum() / defect_by_item[selected_year].sum() * 100
899                            
900                            # 누적 비율 선 그래프
901                            fig.add_trace(go.Scatter(
902                                x=defect_by_item['세부불량'],
903                                y=defect_by_item['누적비율'],
904                                name='누적 비율',
905                                mode='lines+markers',
906                                yaxis='y2',
907                                line=dict(color='royalblue', width=2),
908                                marker=dict(size=8)
909                            ))
910                            
911                            # 80% 기준선
912                            fig.add_trace(go.Scatter(
913                                x=[defect_by_item['세부불량'].iloc[0], defect_by_item['세부불량'].iloc[-1]],
914                                y=[80, 80],
915                                name='80% 기준',
916                                mode='lines',
917                                yaxis='y2',
918                                line=dict(color='green', dash='dash')
919                            ))
920                            
921                            # 그래프 레이아웃 설정
922                            fig.update_layout(
923                                title=f"{selected_defect_type} 세부불량 항목별 파레토 분석 ({selected_year})",
924                                xaxis_title="세부불량 항목",
925                                yaxis_title="불량량",
926                                yaxis2=dict(
927                                    title="누적 비율 (%)",
928                                    overlaying='y',
929                                    side='right',
930                                    range=[0, 100]
931                                ),
932                                legend=dict(
933                                    orientation="h",
934                                    yanchor="bottom",
935                                    y=1.02,
936                                    xanchor="right",
937                                    x=1
938                                ),
939                                height=500
940                            )
941                            
942                            st.plotly_chart(fig, use_container_width=True)
943                        else:
944                            st.warning("세부불량 항목별 데이터가 없습니다.")
945                    else:
946                        st.warning(f"{selected_year} 데이터가 없습니다.")
947                else:
948                    st.warning("연도 데이터가 없습니다.")
949                
950                # 연도별 추이 분석
951                st.markdown("### 연도별 추이 분석")
952                
953                # 세부불량 항목 선택
954                if '세부불량' in type_defects.columns:
955                    items = type_defects['세부불량'].unique()
956                    if len(items) > 0:
957                        selected_item = st.selectbox("세부불량 항목 선택", items)
958                        
959                        # 선택된 세부불량 항목의 연도별 데이터
960                        item_data = type_defects[type_defects['세부불량'] == selected_item]
961                        
962                        if not item_data.empty:
963                            # 연도 컬럼 추출
964                            year_columns = [col for col in item_data.columns if col.endswith('년') and not col.startswith('5개년')]
965                            
966                            if year_columns:
967                                # 연도별 데이터 준비
968                                years = [col.replace('년', '') for col in year_columns]
969                                values = item_data[year_columns].values.flatten().tolist()
970                                
971                                # 연도별 추이 차트
972                                trend_fig = go.Figure()
973                                
974                                # 연도별 불량량 선 그래프
975                                trend_fig.add_trace(go.Scatter(
976                                    x=years,
977                                    y=values,
978                                    mode='lines+markers',
979                                    name='불량량',
980                                    line=dict(color='royalblue', width=2),
981                                    marker=dict(size=8)
982                                ))
983                                
984                                # 그래프 레이아웃 설정
985                                trend_fig.update_layout(
986                                    title=f"{selected_item} 연도별 추이",
987                                    xaxis_title="연도",
988                                    yaxis_title="불량량",
989                                    height=400
990                                )
991                                
992                                st.plotly_chart(trend_fig, use_container_width=True)
993                                
994                                # 5개년 평균 표시
995                                if '5개년 평균' in item_data.columns:
996                                    avg_value = item_data['5개년 평균'].values[0]
997                                    st.metric(label="5개년 평균 ('19~'23)", value=f"{avg_value:,.2f}")
998                            else:
999                                st.warning("연도별 데이터가 없습니다.")
1000                        else:
1001                            st.warning(f"{selected_item} 항목에 대한 데이터가 없습니다.")
1002                    else:
1003                        st.warning("세부불량 항목이 없습니다.")
1004                else:
1005                    st.warning("불량 데이터에 '세부불량' 컬럼이 없습니다.")
1006                
1007                # 목표 달성방안 정보
1008                st.markdown("### 목표 달성방안")
1009
1010                if '목표 달성방안' in type_defects.columns and '세부불량' in type_defects.columns:
1011                    for _, defect in type_defects.iterrows():
1012                        if pd.notna(defect.get('목표 달성방안')):
1013                            # 달성방안 텍스트를 번호가 매겨진 항목으로 분리
1014                            achievement_plans = defect['목표 달성방안'].split('\n')
1015                            
1016                            # 5개년 평균 값 처리
1017                            avg_value = defect.get('5개년 평균', 'N/A')
1018                            avg_display = f"{avg_value:,.2f}" if isinstance(avg_value, (int, float)) else avg_value
1019                            
1020                            st.markdown(f"""
1021                            <div class="defect-box">
1022                                <h4 style="margin-top: 0;">{defect['세부불량']}</h4>
1023                                <p><strong>5개년 평균:</strong> {avg_display}</p>
1024                                <p><strong>목표 달성방안:</strong></p>
1025                                <ul>
1026                                    {"".join(f"<li>{plan.strip('123456789. ')}</li>" for plan in achievement_plans if plan.strip())}
1027                                </ul>
1028                            </div>
1029                            """, unsafe_allow_html=True)
1030                else:
1031                    st.warning("목표 달성방안 정보를 표시하는데 필요한 데이터가 없습니다.")
1032            else:
1033                st.warning(f"{selected_defect_type} 불량구분에 대한 데이터가 없습니다.")
1034        else:
1035            st.warning("불량구분 정보가 없습니다.")
1036    else:
1037        st.warning("불량 데이터에 '불량구분' 컬럼이 없습니다.")
1038
1039def display_customer_analysis(data):
1040    """거래선 분석 표시 함수"""
1041    st.subheader("거래선 분석")
1042    
1043    customer_info = data.get('customer_info', pd.DataFrame())
1044    quality_issues = data.get('quality_issues', pd.DataFrame())
1045    
1046    if customer_info.empty:
1047        st.warning("거래선 정보를 불러올 수 없습니다.")
1048        return
1049    
1050    # 제품군 선택
1051    if '제품' in customer_info.columns:
1052        product_groups = customer_info['제품'].unique()
1053        if len(product_groups) > 0:
1054            selected_group = st.selectbox("제품군 선택", product_groups)
1055            
1056            # 선택된 제품군의 거래선 정보
1057            group_customers = customer_info[customer_info['제품'] == selected_group]
1058            
1059            if not group_customers.empty:
1060                # 거래선 정보 표시
1061                st.markdown("### 거래선 정보")
1062                
1063                customer_row = group_customers.iloc[0]
1064                
1065                col1, col2, col3 = st.columns(3)
1066                
1067                with col1:
1068                    st.metric(label="전체 거래선 수", value=customer_row.get('전체거래선 (개)', "N/A"))
1069                
1070                with col2:
1071                    st.metric(label="제품군", value=selected_group)
1072                
1073                with col3:
1074                    st.metric(label="지역", value=customer_row.get('지역', "N/A"))
1075                
1076                # 주요 거래선 정보
1077                st.markdown("### 주요 거래선 (TOP3)")
1078                
1079                if '주요거래선 (TOP3)' in customer_row:
1080                    top_customers = customer_row['주요거래선 (TOP3)'].split(', ')
1081                    
1082                    for i, customer in enumerate(top_customers):
1083                        st.markdown(f"**{i+1}. {customer}**")
1084                else:
1085                    st.warning("주요 거래선 정보가 없습니다.")
1086            else:
1087                st.warning(f"{selected_group} 제품군에 대한 거래선 정보가 없습니다.")
1088        else:
1089            st.warning("제품군 정보가 없습니다.")
1090    else:
1091        st.warning("거래선 정보에 '제품' 컬럼이 없습니다.")
1092    
1093    # 거래선 관련 품질 이슈 시각화 개선
1094    if not quality_issues.empty:
1095        st.markdown("### 거래선 관련 품질 이슈")
1096        
1097        # 제품군과 관련된 품질 이슈 필터링
1098        filtered_issues = None
1099        if '제품군' in quality_issues.columns:
1100            filtered_issues = quality_issues[quality_issues['제품군'] == selected_group]
1101        elif '제품명' in quality_issues.columns:  # 제품군이 없으면 제품명으로 필터링 시도
1102            filtered_issues = quality_issues[quality_issues['제품명'].str.contains(selected_group, na=False)]
1103        
1104        if filtered_issues is not None and not filtered_issues.empty:
1105            # 연도 필터 추가 (수정된 부분)
1106            if '년도' in filtered_issues.columns:
1107                # 연도 값을 문자열로 변환하고 연도 부분만 추출
1108                years = filtered_issues['년도'].astype(str).apply(lambda x: x[:4] if len(x) >= 4 else x).unique()
1109                years = sorted(years, reverse=True)  # 내림차순 정렬
1110                
1111                selected_year = st.selectbox("연도 선택", years)
1112                
1113                # 선택된 연도로 필터링 (원래 데이터 형식에 맞게)
1114                if pd.api.types.is_datetime64_any_dtype(filtered_issues['년도']):
1115                    # 날짜 형식인 경우
1116                    year_start = pd.to_datetime(f"{selected_year}-01-01")
1117                    year_end = pd.to_datetime(f"{int(selected_year)+1}-01-01")
1118                    year_issues = filtered_issues[(filtered_issues['년도'] >= year_start) & 
1119                                                 (filtered_issues['년도'] < year_end)]
1120                else:
1121                    # 문자열이나 숫자인 경우
1122                    year_issues = filtered_issues[filtered_issues['년도'].astype(str).str.startswith(selected_year)]
1123            else:
1124                year_issues = filtered_issues
1125                selected_year = "전체"  # 연도 필터가 없는 경우 기본값
1126            
1127            # 품질 이슈 데이터 분석을 위한 탭 생성
1128            issue_tabs = st.tabs(["이슈 목록", "거래선별 분석", "원인 분석"])
1129            
1130            with issue_tabs[0]:
1131                # 이슈 목록 표시 (최근 10개)
1132                st.subheader(f"{selected_year}년 이슈 목록")
1133                
1134                if not year_issues.empty:
1135                    for _, issue in year_issues.iterrows():
1136                        # 제품명 가져오기
1137                        product_name = issue.get('제품명', 'N/A')
1138                        
1139                        # 발생결과 정보
1140                        issue_result_major = issue.get('발생결과(대) 1', 'N/A')
1141                        issue_result_minor = issue.get('발생결과(소) 1', 'N/A')
1142                        
1143                        # 거래선 정보
1144                        customer_name = issue.get('거래선명', 'N/A')
1145                        
1146                        # 발생원인 정보
1147                        cause_major = issue.get('발생원인(대) 1', 'N/A')
1148                        cause_minor = issue.get('발생원인(소) 1', 'N/A')
1149                        
1150                        # 귀책 정보
1151                        responsibility = issue.get('귀책', 'N/A')
1152                        
1153                        # 심각도에 따른 색상 설정 (발생결과에 따라)
1154                        severity_color = "#ff7043"  # 기본 색상
1155                        if isinstance(issue_result_major, str):
1156                            if "물성" in issue_result_major:
1157                                severity_color = "#e53935"  # 빨간색 (심각)
1158                            elif "외관" in issue_result_major:
1159                                severity_color = "#fb8c00"  # 주황색 (중간)
1160                            elif "포장" in issue_result_major:
1161                                severity_color = "#66bb6a"  # 녹색 (경미)
1162                        
1163                        st.markdown(f"""
1164                        <div class="issue-box" style="border-left: 4px solid {severity_color}; margin-bottom: 15px;">
1165                            <div style="display: flex; justify-content: space-between; align-items: center;">
1166                                <h4 style="margin: 0;">{issue.get('Q-VOC 번호', 'N/A')}</h4>
1167                                <span style="background-color: {severity_color}; color: white; padding: 2px 8px; border-radius: 4px;">{issue_result_major}</span>
1168                            </div>
1169                            <div style="display: flex; flex-wrap: wrap; margin: 10px 0;">
1170                                <div style="flex: 1; min-width: 200px; margin-right: 10px;">
1171                                    <p><strong>제품명:</strong> {product_name}</p>
1172                                    <p><strong>거래선:</strong> {customer_name}</p>
1173                                </div>
1174                                <div style="flex: 1; min-width: 200px;">
1175                                    <p><strong>채널:</strong> {issue.get('채널', 'N/A')}</p>
1176                                    <p><strong>귀책:</strong> <span style="font-weight: bold; color: #d32f2f;">{responsibility}</span></p>
1177                                </div>
1178                            </div>
1179                            <div style="background-color: #f0f2f6; padding: 10px; border-radius: 4px; margin-bottom: 10px;">
1180                                <div style="display: flex; flex-wrap: wrap;">
1181                                    <div style="flex: 1; min-width: 200px; margin-right: 10px;">
1182                                        <p style="margin: 0;"><strong>발생결과:</strong> {issue_result_major} > {issue_result_minor}</p>
1183                                    </div>
1184                                    <div style="flex: 1; min-width: 200px;">
1185                                        <p style="margin: 0;"><strong>발생원인:</strong> {cause_major} > {cause_minor}</p>
1186                                    </div>
1187                                </div>
1188                            </div>
1189                        </div>
1190                        """, unsafe_allow_html=True)
1191                else:
1192                    st.info(f"{selected_year}년에 해당하는 이슈가 없습니다.")
1193            
1194            with issue_tabs[1]:
1195                # 거래선별 이슈 분석
1196                st.subheader(f"{selected_year}년 거래선별 이슈 분석")
1197                
1198                if '거래선명' in year_issues.columns:
1199                    # 거래선별 이슈 수 계산
1200                    customer_issues = year_issues['거래선명'].value_counts().reset_index()

Showing the first 1,200 of 1445 lines. Download the file for the rest.