lscsc/Process_dash_pub
0
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()
