CoolFace
Apppublic

lscsc/Process_dash_pub

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

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