심화프로젝트 2일차 - 데이터 전처리 및 가설검증
1. 이상치 확인 및 처리
프로젝트를 진행하면서 아마 가장 어려운 부분인것 같습니다.

이런식으로 원본데이터에 1.5XIQR을 넘는 데이터가 너무 많아서 어떻게 데이터를 처리해야할지 난제에 빠졌습니다.
우선 상식적으로 말이 안되는 수치를 제거했습니다.
#극단값 제거하기
df = df[df['Pixels_Areas'] < 150000]
df = df[df['X_Perimeter'] < 10000]
df = df[df['Y_Perimeter'] < 18000]
그리고 데이터에 로그를 씌워서 어느정도 정규성을 가지게 해보았습니다.
# 로그 변환 후 IQR 확인
import numpy as np
# df_log를 df의 복사본으로 초기화하여 원본 데이터프레임 df를 보호합니다.
df_log = df.copy()
# 로그 변환 대상 컬럼 이름 리스트
cols_to_log = ['Pixels_Areas', 'X_Perimeter', 'Y_Perimeter','X_Minimum','X_Maximum','Y_Minimum',
'Y_Maximum']
# 로그 변환 (0이 있으면 log(0) 에러 → +1 해서 회피)
for col_name in cols_to_log:
# 컬럼이 존재하는지 확인 후 변환 적용
if col_name in df_log.columns:
df_log[f'{col_name}_log'] = np.log1p(df_log[col_name])
else:
print(f"경고: df_log에 '{col_name}' 컬럼이 없습니다. 이 컬럼에 대한 로그 변환을 건너뜁니다.")
# 변환된 데이터 확인
transformed_cols = [f'{c}_log' for c in cols_to_log if f'{c}_log' in df_log.columns]
if transformed_cols:
display(df_log[transformed_cols].head())
else:
print("로그 변환된 컬럼이 생성되지 않았습니다.")
그리고 해당 데이터를 기반으로 다시 이상치를 파악하니
# 위치 기반 컬럼 이상치제거(IQR,LOG 사용)
# 로그 변환 후 IQR 기반 이상치 카운트
import pandas as pd
import numpy as np
# df_log는 이전 셀에서 이미 DataFrame으로 생성되고 로그 변환이 적용되었다고 가정합니다.
# 확인할 컬럼 리스트 (로그 변환된 컬럼들)
cols_for_outliers = [f'{c}_log' for c in ['Pixels_Areas', 'X_Perimeter', 'Y_Perimeter','X_Minimum','X_Maximum','Y_Minimum',
'Y_Maximum']]
for col in cols_for_outliers:
# 해당 컬럼이 df_log에 존재하는지 확인
if col not in df_log.columns:
print(f"경고: df_log에 '{col}' 컬럼이 없습니다. 다음 컬럼으로 넘어갑니다.")
continue
Q1 = df_log[col].quantile(0.25)
Q3 = df_log[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 오류 수정: df[col] 대신 df_log[col] 사용
outliers = df_log[(df_log[col] < lower_bound) | (df_log[col] > upper_bound)]
print(f" [{col}] 이상치 요약")
print(f" - Q1: {Q1:.2f}, Q3: {Q3:.2f}, IQR: {IQR:.2f}")
print(f" - 하한: {lower_bound:.2f}, 상한: {upper_bound:.2f}")
print(f" - 이상치 개수: {len(outliers)}개")
print(outliers[[col]].head())

이런식으로 이상치가 확연히 줄어든것을 볼 수 있습니다.
그리고 저는 여기서 정규화된 데이터를 바이올린 플롯으로 그려보았습니다.
fig, axes = plt.subplots(nrows=3, ncols=2, figsize=(14, 15))
sns.violinplot(df, y='Log_X_Index', x='Pastry', hue='Pastry', ax=axes[0][0])
axes[0][0].set_title('Log_X_Index', fontsize=14)
sns.violinplot(df, y='Log_Y_Index', x='Pastry', hue='Pastry', ax=axes[0][1])
axes[0][1].set_title('Log_Y_Index', fontsize=14)
sns.violinplot(df_log, y='Pixels_Areas_log', x='Pastry', hue='Pastry', ax=axes[1][0])
axes[1][0].set_title('Pixels_Areas_log', fontsize=14)
sns.violinplot(df_log, y='X_Perimeter_log', x='Pastry', hue='Pastry', ax=axes[1][1])
axes[1][1].set_title('X_Perimeter_log', fontsize=14)
sns.violinplot(df_log, y='Y_Perimeter_log', x='Pastry', hue='Pastry', ax=axes[2][0])
axes[2][0].set_title('Y_Perimeter_log', fontsize=14)
axes[2][1].set_visible(False)
plt.suptitle('Pastry결함에 대한 영향도', fontsize=18, y=1.00)
plt.tight_layout()
plt.show()

이렇게 보니 간단한 가설을 설정할 수 있었습니다.
| 1. Pastry 결함 pixels_areas - 평균 비슷, 상대적으로 좁은 범위에 분포 length of conveyer - 평균값 높음운송거리가 많을 수록 더 많은 결함 발생 철반두께 - 이상치 영향 있음, 평균값 높음두께는 근소하게 영향을 미침 방향도 - 양의 방향에 치중되어 있음 대체적으로 양의 방향일 때 결함발생 |
이런식으로 7가지 결함에 대해 간단한 가설을 세웠고 이를 검증하기 위해 t-test를 실시하기 위해 정규성을 검토했습니다.

아쉽게도 대부분의 데이터들이 정규성을 띄지 않아서 t-test 대신 Mann-Whitney U Test를 진행했습니다.
from scipy.stats import ttest_ind
t_state_pastry_Pixels, p_value_pastry_Pixels = ttest_ind(df_pastry['Pixels_Areas'], df_no_pastry['Pixels_Areas'])
t_state_pastry_XPerimeter, p_value_psstry_XPerimeter = ttest_ind(df_pastry['X_Perimeter'], df_no_pastry['X_Perimeter'])
t_state_pastry_YPerimeter, p_value_psstry_YPerimeter = ttest_ind(df_pastry['Y_Perimeter'], df_no_pastry['Y_Perimeter'])
t_state_pastry_Conveyer, p_value_psstry_Conveyer = ttest_ind(df_pastry['Length_of_Conveyer'], df_no_pastry['Length_of_Conveyer'])
t_state_pastry_Thickness, p_value_psstry_Thickness = ttest_ind(df_pastry['Steel_Plate_Thickness'], df_no_pastry['Steel_Plate_Thickness'])
t_state_pastry_Orientation, p_value_pastry_Orientation = ttest_ind(df_no_pastry['Orientation_Index'], df_pastry['Orientation_Index'])
print('Pastry 결함면적 검정')
print(f't_stat: {t_state_pastry_Pixels}, p_value: {p_value_pastry_Pixels}')
print('Pastry 결함 X_경계길이 검정')
print(f't_stat: {t_state_pastry_XPerimeter}, p_value: {p_value_psstry_XPerimeter}')
print('Pastry 결함 Y_경계길이 검정')
print(f't_stat: {t_state_pastry_YPerimeter}, p_value: {p_value_psstry_YPerimeter}')
print('Pastry 결함 이송거리 검정')
print(f't_stat: {t_state_pastry_Conveyer}, p_value: {p_value_psstry_Conveyer}')
print('Pastry 결함 철판두께 검정')
print(f't_stat: {t_state_pastry_Thickness}, p_value: {p_value_psstry_Thickness}')
print('Pastry 결함 방향성 검정')
print(f't_stat: {t_state_pastry_Orientation}, p_value: {p_value_pastry_Orientation}')
Pastry 결함면적 검정
t_stat: -4.267306179974067, p_value: 2.0738854609100897e-05
Pastry 결함 X_경계길이 검정
t_stat: -4.960117552235953, p_value: 7.659634338012978e-07
Pastry 결함 Y_경계길이 검정
t_stat: -1.7968736687700153, p_value: 0.07251133180811208
Pastry 결함 이송거리 검정
t_stat: 8.804720037422044, p_value: 2.846470783045391e-18
Pastry 결함 철판두께 검정
t_stat: 3.376867030394855, p_value: 0.0007476528286841543
Pastry 결함 방향성 검정
t_stat: -15.387029897600591, p_value: 1.7062078466951517e-50
이런식으로 결과가 나오게 되었고 기존 시각화로 세운 가설과 비교해 보니 일치하는것을 확인했습니다.
![]() |
Pastry 결함(a=0.05) H0 : 결함의 면적과 정상의 면적은 같다 H1 : 결함의 면적과 정상의 면적은 다르다 -> H0 채택 : 결함의 면적과 정상의 면적은 같다. H0 : 결함과 정상의 X경계길이는 같다. H1 : 결함의 정상의 X경계길이는 다르다. -> H1 채택 : 결함의 정상의 X경계길이는 다르다. H0 : 결함과 정상의 Y경계길이는 같다. H1 : 결함의 정상의 Y경계길이는 다르다. -> H1 채택 : 결함의 정상의 Y경계길이는 다르다. H0 : 결함과 정상의 이송거리는 같다. H1 : 결함과 정상의 이송거리는 다르다. -> H1 채택 : 결함과 정상의 이송거리는 다르다. H0 : 결함과 정상의 철판 두께는 같다. H1 : 결함과 정상의 철판 두께는 다르다. -> H1 채택 : 결함과 정상의 철판 두께는 다르다. H0 : 결함과 정상의 방향성은 같다. H1 : 결함과 정상의 방향성은 다르다. -> H1 채택 : 결함과 정상의 방향성은 다르다. |
2. 발생한 문제점
아직도 이상치를 어떤 기준으로 처리해야하는지에 대해 결정하지 못했습니다.
일단 머신러닝을 돌려보고 이상치를 제거한 데이터와 제거하지 않은 데이터의 정확도를 비교해보고 조원들끼리 토의 해보기로 했습니다.
튜터님께서도 이부분이 가장 큰 난제라고 하셨고 아마 여러번 코딩을 해봐야 어느정도 문제가 해결될 것 같습니다.
3. 느낀점
항상 느끼는 거지만 t-test 기사문제로 엄청 어렵게 나오는데 이렇게 버튼 몇개로 해결할 수 있다니... QA/QC 능력에 필수로 데이터 활용능력에 필수적인 것 같습니다. 더군다나 정규화 되지 않은 데이터도 비교검증을 하는 Mann-Whitney U Test 을 실제로 활용해보니 실무에서도 활용할 수 있다는 자신감을 얻었습니다.
