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 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;">
