공정 코드마다 그림을 하나씩 만들고, 그 안에서 설비별로 색을 달리해 지표의 추세를 보고 싶다. 눈으로 차이가 보이면 그 차이가 우연인지 아닌지까지 확인해야 한다. 반복문 두 겹과 사후 검정을 붙이는 형태가 된다.
범례에 그룹 이름이 나오려면 scatter 를 그룹별로 나눠 호출하고 label 을 준다. 한 번에 그리면서 c= 로 색만 나눠 주면 범례가 만들어지지 않는다.
import matplotlib
import matplotlib.pyplot as plt
def plot_by_group(df, x, y, group_col, title):
groups = df[group_col].unique()
cmap = matplotlib.colormaps['tab20'].resampled(len(groups))
fig, ax = plt.subplots(figsize=(10, 6))
for i, g in enumerate(groups):
sub = df[df[group_col] == g].sort_values(by=x)
ax.scatter(sub[x], sub[y], s=8, color=cmap(i), label=str(g))
ax.set_xlabel(x)
ax.set_ylabel(y)
ax.set_title(title)
ax.legend(title=group_col, bbox_to_anchor=(1.02, 1), loc='upper left', fontsize=8)
fig.autofmt_xdate(rotation=45)
fig.tight_layout()
return fig, ax
plt.cm.get_cmap('tab20', n) 형태는 matplotlib 3.7 에서 폐기되고 3.9 에서 제거됐다. 위처럼 matplotlib.colormaps[...] 를 쓴다.
tab20 은 색이 스무 개다. 그룹이 그보다 많으면 색이 돌아가 구분되지 않는다. 이때는 마커 모양을 함께 돌리거나 그룹을 나눠 여러 장으로 그린다.
markers = ['o', 's', '^', 'D', 'v', 'P', 'X', '*']
ax.scatter(sub[x], sub[y], s=8, color=cmap(i % 20),
marker=markers[i % len(markers)], label=str(g))
범례가 그림을 가리면 bbox_to_anchor 로 바깥으로 빼고 tight_layout() 을 부른다. 이 둘을 같이 쓰지 않으면 범례가 잘린다.
목표치 같은 상수를 가로선으로 긋는다.
ax.axhline(y=target_ppm, color='red', linestyle='--', linewidth=1,
label=f'목표 {target_ppm}')
기준이 상수가 아니라 관측값에서 계산되는 값이라면 axhline 이 아니라 계산한 시리즈를 plot 으로 얹는다. 평균선이면 df[y].mean() 을 axhline 에 넣는다.
그림만으로는 "달라 보인다" 까지다. 전체에 차이가 있는지를 일원분산분석으로 먼저 보고, 유의하면 어느 쌍이 다른지를 Tukey HSD 로 본다.
import pandas as pd
import scipy.stats as stats
from statsmodels.stats.multicomp import pairwise_tukeyhsd
def compare_groups(df, value_col, group_col, alpha=0.05):
samples = [g[value_col].dropna().values for _, g in df.groupby(group_col)]
samples = [s for s in samples if len(s) >= 2]
if len(samples) < 2:
return None
f_stat, p_value = stats.f_oneway(*samples)
print(f'ANOVA F={f_stat:.4f} p={p_value:.6f}')
if p_value >= alpha:
return None
tukey = pairwise_tukeyhsd(endog=df[value_col], groups=df[group_col], alpha=alpha)
table = tukey.summary()
tukey_df = pd.DataFrame(table.data[1:], columns=table.data[0])
means = df.groupby(group_col)[value_col].mean()
tukey_df['group1_mean'] = tukey_df['group1'].map(means)
tukey_df['group2_mean'] = tukey_df['group2'].map(means)
return tukey_df
tukey.summary() 를 쓴다. 예제에서 자주 보이는 tukey_result._results_table 은 밑줄로 시작하는 내부 속성이라 버전이 오르면 사라질 수 있다.
결과 표의 reject 열이 True 인 행이 유의한 차이가 있는 쌍이다. meandiff 는 group2 - group1 이고 lower · upper 는 신뢰구간이다. 구간이 0 을 포함하면 reject 는 False 가 된다.
stats.f_oneway 대신 pingouin.welch_anova)이나 비모수 검정(stats.kruskal)을 본다.