TIL(Today I Learned)
1️⃣심화 프로젝트
통계 검정
Shapiro-Wilk W 통계량 막대 + Q-Q plot
더보기
cols = ['TPS', 'Power', 'RPM', 'Consumption L/H',
'HC', 'CO_CO2_ratio', 'CO_log',
'O2_RPM_Effect', 'lambda_dev', 'O2']
# Shapiro-Wilk (n>5000 → 샘플링)
for col in cols:
sample = df_clean[col].sample(5000, random_state=42)
stat, p = stats.shapiro(sample)
skew = stats.skew(df_clean[col])
print(f"{col}: W={stat:.4f}, p={p:.2e}, skew={skew:.2f}")
# QQ-plot 시각화
plt.figure(figsize=(15,10))
for i, col in enumerate(cols):
plt.subplot(3, 4, i+1)
sample = df_clean[col].sample(5000, random_state=42)
stats.probplot(sample, dist="norm", plot=plt)
plt.title(col)
plt.tight_layout()
plt.show()
# 출력값
TPS: W=0.6057, p=4.27e-75, skew=2.35
Power: W=0.6366, p=2.11e-73, skew=2.12
RPM: W=0.9171, p=4.21e-46, skew=0.85
Consumption L/H: W=0.7895, p=1.46e-62, skew=2.16
HC: W=0.3471, p=5.03e-86, skew=6.16
CO_CO2_ratio: W=0.6021, p=2.73e-75, skew=3.02
CO_log: W=0.8577, p=2.50e-55, skew=1.23
O2_RPM_Effect: W=0.7404, p=1.30e-66, skew=1.99
lambda_dev: W=0.7744, p=6.94e-64, skew=2.05
O2: W=0.9370, p=9.41e-42, skew=0.53

Q-Q Plot 해석 (정규성 확인)
- 비정상적 분포
- TPS, Power, Consumption L/H, CO_CO2_ratio
- 부드러운 곡선X / 계단식으로 끊어져 나타남
- 오른쪽 끝(상위 값)에서 선을 이탈해 급격히 치솟는 모습 -> 심한 이상치 존재
- HC
- 대부분의 데이터가 아래쪽에 몰려 있다가 마지막에 급격히 상승
- TPS, Power, Consumption L/H, CO_CO2_ratio
- 비교적 양호하지막 왜곡된 분포
- RPM, O2
- 다른 변수들에 비해 점들이 빨간 선을 따라가지만, 양 끝(꼬리 부분)에서는 선을 벗어남
- 완전한 정규분포는 아니지만, 분석 모델에서 그나마 다루기 수월한 변수들
- CO_log
- 로그 변환을 했음에도 불구하고 양 끝단에서 두껍게 나타나는 분포를 보임
- RPM, O2
요약
모든 데이터가 정규분포와는 거리가 멀기 때문에, 비모수 분석이 훨씬 정확한 결과를 준다.
Kruskal-Wallis H 통계량 검정
더보기
# Kruskal-Wallis
results = []
for var in cols:
groups = [df_clean[df_clean['Fault']==f][var].values for f in [0,1,2,3]]
H, p = stats.kruskal(*groups)
results.append([var, H, p]) # p 그대로 사용
res_df = pd.DataFrame(results, columns=['var', 'H', 'p'])
# log 변환 (0 방지)
res_df['log_p'] = -np.log10(res_df['p'].replace(0, 1e-300))
res_df = res_df.sort_values('H', ascending=True)
# 시각화
fig, ax = plt.subplots(figsize=(15, 7))
sc = ax.scatter(
res_df['H'],
res_df['var'],
c=res_df['log_p'],
s=200,
cmap='viridis',
edgecolors='black',
linewidths=0.5
)
# p값 텍스트 표시
for _, row in res_df.iterrows():
p_text = f"p={row['p']:.1e}" if row['p'] < 0.05 else f"p={row['p']:.3f}"
ax.text(row['H'] + 100, row['var'], p_text, va='center', fontsize=8)
plt.colorbar(sc, ax=ax, label='-log10(p)')
ax.set_xlabel('Kruskal H-통계량 (차이의 크기)', fontsize=11)
ax.set_ylabel('엔진 센서 변수', fontsize=11)
ax.set_title('Kruskal-Wallis 검정', fontsize=12)
ax.set_xlim(0, res_df['H'].max() * 1.2)
ax.grid(axis='x', linestyle='--', alpha=0.5)
plt.tight_layout()
plt.show()

Kruskal-Wallis Analysis (No Correction) 그래프 해석
- 주요 지표 해석 기준
- X축(Kruskal H-statistic)
- 집단 간 차이의 크기를 의미한다. 값이 클수록 해당 센서가 결함 유형(Fault 0~3)을 구분하는 데 결정적인 역할을 한다.
- Y축
- 분석에 사용된 엔진 센서 항목들
- 색상 및 우측 바
- 통계적 유의성을 나타낸다. 노란색에 가까울수록 p-값이 매우 작아 우연히 발생했을 확률이 거의 없는, 즉 매우 확실한 차이를 의미한다.
- 점 크기 = 영향력 (H 기반) 큰 원 → 모델에 영향 큼 /작은 원 → 영향 거의 없음
- 진한 파랑 = p값 매우 작음 = 확실한 차이
- X축(Kruskal H-statistic)
- 변수별 상세 분석
- HC (탄화수소), CO_log, CO_CO2_ratio, lambda_dev, RPM
- H-statistic이 매우 높고(약 4,000 이상), 값이 거의 0()에 수렴한다.0.0e+00
- p
- 해석: 이 센서들은 엔진 결함 발생 시 수치 변화가 가장 뚜렷하다. 특히 HC와 CO 관련 지표들의 H값이 압도적으로 높은데, 이는 엔진 연소 상태와 배출가스 데이터가 결함 예측의 핵심 변수임을 알 수 있다.
- O2_RPM_Effect, TPS, Power
- H-statistic이 500~1,000 사이에 위치하며, 값은 매우 낮지만 최상위 지표들보다는 영향력이 적다.
- p
- 해석: 결함 구분에 유의미한 도움을 주지만, 배출가스 데이터만큼 강력한 구분 기준은 아니다.
- Consumption L/H, O2
- H-statistic이 가장 낮고 보라색에 가깝다.
- 해석: 물론 로 통계적 유의성은 확보했으나, 다른 센서들에 비해 결함 유형별 데이터의 중첩이 많아 변별력이 상대적으로 떨어진다.
- p<0.05
- HC (탄화수소), CO_log, CO_CO2_ratio, lambda_dev, RPM
Dunn's test 히트맵
더보기
cols = ['TPS', 'Power', 'RPM', 'Consumption L/H',
'HC', 'CO_CO2_ratio', 'CO_log',
'O2_RPM_Effect', 'lambda_dev', 'O2']
pairs = ['0 vs 1', '0 vs 2', '0 vs 3', '1 vs 2', '1 vs 3', '2 vs 3']
# Dunn's test 전체 변수 실행 후 2 vs 3 쌍 추출
p_results = {}
for var in cols:
sample = pd.concat([
df_clean[df_clean['Fault']==f][['Fault', var]]
.sample(3000, random_state=42)
for f in [0,1,2,3]
])
dunn = sp.posthoc_dunn(sample, val_col=var,
group_col='Fault',
p_adjust='bonferroni')
p_results[var] = {
'0 vs 1': dunn.loc[0, 1],
'0 vs 2': dunn.loc[0, 2],
'0 vs 3': dunn.loc[0, 3],
'1 vs 2': dunn.loc[1, 2],
'1 vs 3': dunn.loc[1, 3],
'2 vs 3': dunn.loc[2, 3],
}
p_df = pd.DataFrame(p_results).T
# 시각화
fig, ax = plt.subplots(figsize=(10, 7))
cmap = plt.cm.get_cmap('Blues_r')
im = ax.imshow(p_df.values, cmap=cmap, aspect='auto', vmin=0, vmax=1)
for i, var in enumerate(p_df.index):
for j, pair in enumerate(p_df.columns):
val = p_df.loc[var, pair]
if val < 0.001:
label = '***'
color = 'white'
elif val < 0.05:
label = f'{val:.3f}\n*'
color = 'white'
else:
label = 'ns'
color = '#888888'
ax.text(j, i, label, ha='center', va='center',
fontsize=10, color=color, fontweight='bold')
ax.set_xticks(range(len(pairs)))
ax.set_xticklabels(pairs, fontsize=11)
ax.set_yticks(range(len(p_df.index)))
ax.set_yticklabels(p_df.index, fontsize=11)
plt.colorbar(im, ax=ax, label='p-value (lower = more significant)')
ax.set_title("Dunn's Test 결과 (본페로니 보정)\n*** p<0.001 * p<0.05 ns = 연관성 없음",
fontsize=12)
plt.tight_layout()
plt.grid(False)
plt.show()

Dunn's Test Result (Bonferroni correction) 그래프 해석
- 주요 지표 해석 기준
- x축
- 비교 대상인 결함 유형의 쌍이다.
- y축
- 분석에 사용된 엔진 센서 항목들
- 색상
- 색이 짙은 파란색일수록 값이 낮아 통계적 유의성이 높음을 의미한다.
- p
- 기호 해석
- **: (매우 강력한 유의성)
- p<0.001
- : (통계적으로 유의미함)
- p<0.05
- ns (not significant): 통계적으로 유의미한 차이가 없음 (두 결함 유형 간 센서 값 분포가 비슷함)
- x축
- 변수별 상세분석
- 변별력이 높은 센서 (짙은 파란색이 많은 행)
- HC (탄화수소)
- 모든 결함 쌍( 포함)에 대해 유의미한 차이를 보인다. 이는 엔진 결함 유형을 구분하는 데 가장 핵심적인 지표임을 뜻한다.
- 2 vs 3
- O2_RPM_Effect, RPM, CO_log
- 대부분의 결함 조합을 효과적으로 구분해내고 있다.
- HC (탄화수소)
- 특정 결함 쌍에 취약한 센서 (흰색/ns가 많은 열)
- 0 vs 1
- TPS, Power, Consumption L/H, HC를 제외한 나머지 지표들은 ns가 많다. 즉, 0번과 1번 결함을 구분할 때는 배출가스 농도보다는 물리적인 출력이나 스로틀(TPS) 데이터가 더 유용할 수 있다.
- 2 vs 3
- 가장 구분이 어려운 쌍으로 보인다. 많은 지표가 ns를 보이지만, HC, O2_RPM_Effect, RPM 등은 여전히 유의미한 차이(*)를 유지하며 두 결함을 구분하는 기준이 되어준다.
- 0 vs 1
- 변별력이 높은 센서 (짙은 파란색이 많은 행)
Cohen's d 막대 + O2_RPM_Effect KDE
더보기
results = []
sub = df_clean[df_clean['Fault'].isin([2,3])].copy()
for var in cols:
g2 = sub[sub['Fault']==2][var]
g3 = sub[sub['Fault']==3][var]
pooled_std = np.sqrt((g2.std()**2 + g3.std()**2) / 2)
d = (g2.mean() - g3.mean()) / pooled_std
results.append([var, d])
d_df = pd.DataFrame(results, columns=['var','cohens_d'])
d_df = d_df.sort_values('cohens_d')
plt.figure(figsize=(10,6))
plt.barh(d_df['var'], d_df['cohens_d'])
# 기준선
plt.axvline(0, linestyle='--') # +, -
plt.axvline(0.2, linestyle=':') # 낮음
plt.xlabel("효과 크기(Cohen's d)")
plt.title("변수별 효과크기 분석(결함 2vs3)")
plt.show()

변수 별 효과크기 그래프 해석
- 주요 지표 해석 기준
- X축 (Cohen's d)
- 두 집단 평균 차이를 표준편차로 나눈 값이다. 0에서 멀어질수록(절대값이 클수록) 두 결함 유형 간의 차이가 실질적으로 크다는 것을 의미다.
- y축(Features)
- 분석 대상이 된 엔진 센서 변수들이다.
- 수직 점선 (관습적 기준)
- 0.2 (Small): 작은 효과 크기
- 0.5 (Medium): 중간 효과 크기
- 0.8 (Large): 큰 효과 크기
- X축 (Cohen's d)
- 센서별 상세 진단
- 가장 큰 차이를 보이는 변수
- O2 (산소 센서)
- 양(+)의 방향으로 가장 큰 막대를 가졌다. 결함 3에 비해 결함 2에서 산소 농도가 상대적으로 높게 나타나는 경향이 있으며, 실질적인 변별력이 가장 높다.
- HC (탄화수소)
- 음(-)의 방향으로 가장 큰 막대를 보인다. 이는 결함 2보다 결함 3에서 HC 배출량이 더 높게 측정될 가능성이 크다는 것을 시사한다.
- O2 (산소 센서)
- 중간 정도의 차이
- RPM, O2_RPM_Effect
- 0에서 어느 정도 떨어져 있어 결함 구분에 보조적인 도움을 준다.
- RPM, O2_RPM_Effect
- 차이가 거의 없는 변수
- Consumption L/H, TPS, lambda_dev
- 막대가 거의 0에 가깝다. 통계적 유의성과 상관없이, 실질적인 수치 분포 면에서는 결함 2와 3 사이에서 큰 차이가 없음을 나타난다.
- Consumption L/H, TPS, lambda_dev
- 가장 큰 차이를 보이는 변수
결론
- 실질적 변별력 확인
- 현재 모든 변수의 Cohen's d 절대값이 0.2(Small) 미만으로 나타난다. 이는 결함 2와 3이 데이터적으로 매우 유사한 성격을 가지고 있어, 단일 센서 값만으로는 완벽하게 분리하기 어렵다는 것을 뜻한다.
- 모델링 전략
- 효과 크기가 미미하므로, 단일 변수보다는 O2와 HC의 조합 또는 여러 센서 데이터를 동시에 사용하는 머신러닝 앙상블 모델을 활용해야 결함 2와 3을 더 정확하게 분류할 수 있다.
💡 느낀점
전처리와 스케일링 등의 과정을 거친 정제된 데이터를 통계검정을 통해 정말 유의미한지? 생성한 파생변수가 연관성을 갖는지 확인할 수 있었다. 가설과 완전히 동일한 결과가 나오지는 않았지만, 어느정도의 연관성을 띄었다. 추후에는 다양한 모델들을 비교하여 결함 2,3을 높은 정확도로 분류할 수 있는 모델을 찾고, 그 모델 내에서도 파라미터 튜닝 등의 시행착오를 겪으며 문제를 풀어나갈 예정이다.
'심화 프로젝트' 카테고리의 다른 글
| Chapter.5 심화 프로젝트(6) (0) | 2026.05.08 |
|---|---|
| Chapter.5 심화 프로젝트(5) (0) | 2026.05.07 |
| Chapter.5 심화 프로젝트(3) (0) | 2026.05.01 |
| Chapter.5 심화 프로젝트(2) (0) | 2026.04.30 |
| Chapter.5 심화 프로젝트(1) (0) | 2026.04.29 |