노이즈가 심할수록 점수가 올랐다: 자기 IQR로 이상치를 재던 지표

안녕하세요, 고준서입니다. 캡스톤 팀에서 데이터 품질 점수 엔진(DSC)과 실험을 맡았습니다. 4월 12일에 첫 엔진이 결측을 못 세던 문제를 고치고 나서 4개 노트북을 끝까지 돌렸는데, 점수와 모델 성능 사이 피어슨 상관계수 r이 0.0846(p=0.147)이 나왔습니다. 그대로 두면 “품질 점수는 모델 성능과 무관하다”가 결론이 되는 숫자였습니다.

원인은 이상치 지표가 지금 보고 있는 데이터의 IQR을 기준으로 재고 있어서, 노이즈를 더 넣을수록 기준이 같이 넓어지던 것이었어요. 이 글에서는 전체 상관 0.08을 오염 종류별로 쪼개 그 지표 하나에 닿기까지의 순서와, 기준을 원본 분포에 고정한 뒤 0.42까지 간 전후 숫자, 그리고 못 고친 것을 진단 보고서와 커밋에서 옮겨 적습니다.

오염 종류별로 쪼개니 부호가 반대였습니다

4월 25일에 결과 파일 merged_results.csv를 오염 종류와 데이터셋별로 나눠 상관을 다시 냈습니다. 같은 데이터셋, 같은 오염 안에서 모델 5개 × 강도 4단계 = 20점으로요.

오염 종류 Telco SouthGermanCredit letter
completeness +0.530 +0.599 +0.639
consistent_repr +0.479 +0.728 n/a
class_balance +0.608 +0.800 +0.104
uniqueness +0.085 +0.458 −0.009
feature_accuracy +0.775 −0.891 −0.779

표 1. 오염 종류 × 데이터셋별 r(진단 보고서 20260425-01). 결측과 표현 불일치는 잘 되고 있었습니다.

결측(completeness)과 표현 불일치는 0.5에서 0.7 사이로 멀쩡했어요. 문제는 값 오류(feature_accuracy)였습니다. Telco에서는 +0.78인데 나머지 두 데이터셋에서는 −0.89, −0.78. 같은 오염인데 데이터셋에 따라 부호가 뒤집혔고, 이게 한 풀에 섞이면서 전체가 0 근처로 무너진 거였습니다.

이상치 지표가 자기 자신을 기준으로 재고 있었습니다

값 오류 오염 강도별로 outlier_ratio 지표만 뽑아 보면 이렇습니다.

dataset 0.0 0.1 0.25 0.5 0.75
Telco 1.0000 0.9997 0.9993 0.9969 0.9956
SouthGermanCredit 0.9450 0.9540 0.9670 0.9740 0.9873
letter 0.9671 0.9772 0.9867 0.9906 0.9918

표 2. 노이즈를 더 넣을수록 “이상치가 적다”는 점수가 나왔습니다.

원인은 calc_outlier_ratio가 IQR(사분위 범위)을 지금 보고 있는 데이터에서 계산한다는 점이었습니다. q1, q3 = s.quantile(0.25), s.quantile(0.75). 그런데 FeatureAccuracyPolluter는 수치 열에 N(0, level) * col_mean 크기의 가우시안 노이즈를 더합니다. 노이즈가 커지면 분산이 커지고, 분산이 커지면 IQR도 같이 넓어지고, IQR이 넓어지면 이상치 경계도 넓어져서 이상치로 잡히는 비율이 줄어듭니다. 변화가 측정 기준 안으로 흡수되는 구조였어요. 진단 보고서에는 “자기참조 함정”이라고 적었습니다.

letter에서 이 지표 하나가 강도 0.75에서 DSC 총점을 기준선보다 1.58점 올렸습니다. 같은 조건에서 letter의 F1은 0.957에서 0.46으로 떨어졌고요. 점수는 올라가고 성능은 떨어지니 상관이 음수가 될 수밖에 없었습니다.

고친 건 기준을 원본 데이터의 IQR로 고정하는 거였습니다(d6fe933).

def calc_outlier_ratio(df, target_col, numerical_cols, reference_df=None):
    """IQR 기반 outlier가 아닌 비율.
    v3: reference_df의 IQR을 고정 기준으로 사용 → 노이즈가 IQR을 부풀려 outlier가
    줄어드는 자기참조 함정 회피."""
    ...
        if reference_df is not None and col in reference_df.columns:
            ref = pd.to_numeric(reference_df[col], errors='coerce').dropna()
            if len(ref) >= 4:
                q1, q3 = ref.quantile(0.25), ref.quantile(0.75)
            else:
                q1, q3 = s.quantile(0.25), s.quantile(0.75)
        else:
            q1, q3 = s.quantile(0.25), s.quantile(0.75)
        iqr = q3 - q1
        ...
        lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
        outlier_count = ((s < lower) | (s > upper)).sum()
        scores.append(1.0 - outlier_count / len(s))

letter 값 오류 0.75에서 outlier_ratio가 0.992에서 0.697로 내려왔습니다. 방향이 맞아진 거죠.

같은 진단에서 값 오류가 안 보이던 이유가 하나 더 있었습니다. validity 지표가 값이 아니라 타입만 검사하고 있었어요. 수치형은 pd.to_numeric이 되는지, 범주형은 길이가 0에서 200 사이인지. 노이즈를 더해도 숫자는 숫자고, 범주값을 같은 열의 다른 값으로 바꿔도 타입과 도메인이 그대로니 아무것도 안 걸립니다. 그래서 원본 분포와의 거리를 재는 value_accuracy를 새로 넣었습니다. 수치형은 KS 통계량, 범주형은 총변동거리(TVD)로요. 이걸 넣고 letter 값 오류 0.75의 DSC가 99.69에서 91.12로 내려왔습니다. 드디어 오염이 점수에 보인 겁니다.

오후 한 시간 동안 세 번 커밋했습니다

13시 57분에 outlier 기준 고정과 value_accuracy 신설(v3), 14시 00분에 class_balance 가중치 0.05를 0.10으로(v3.1), 14시 10분에 consistency 지표 교체(v3.2)였습니다.

class_balance는 가중치가 5%라 최소 클래스가 0까지 떨어져도 점수 영향이 5점이 천장이었습니다. 26개 클래스인 letter에서는 이상적 비율이 1/26이라 최소 비율이 0.01까지 떨어져야 겨우 반응하기도 했고요. 가중치를 두 배로 올리고, 타입만 검사해서 무력해진 validity에서 자리를 가져왔습니다.

consistency는 4월 12일에 고친 뒤에도 문제가 남아 있었습니다. 접미사 -숫자가 붙은 행 비율을 재는 방식이었는데, 그건 오염 도구가 새 표현을 만들 때 쓰는 명명 규칙을 그대로 매칭한 거라 실험 안에서만 통하는 지표였어요. 실제 데이터의 “iPhone, IPHONE, i-phone” 같은 불일치는 못 잡습니다. 원본에 없던 새 표현이 차지하는 행 비율로 바꿨고, 강도 0.1, 0.25, 0.5, 0.75에서 0.90, 0.75, 0.50, 0.25로 정확히 비례하는 걸 확인했습니다.

기존 오염 데이터에 v3.2 엔진을 다시 적용해 시뮬레이션한 전체 r은 0.0846에서 0.358(p=2.4e-10)로 올랐고, Colab에서 정식으로 다시 돌린 결과는 0.420이었습니다. C, D 등급이 0건이던 분포에 C 5건, D 3건이 생겨 등급별 비교도 가능해졌습니다.

못 고친 것도 있습니다

중복(uniqueness) 오염은 여전히 상관이 거의 없습니다. UniquenessPolluter는 train의 일부 행을 그대로 복제하는데, 트리 모델은 분할 기준이 안 바뀌고 선형 모델은 같은 샘플의 손실 기여도만 커질 뿐이라 test 성능이 거의 안 변합니다. 반면 DSC의 uniqueness 지표는 행 수에 그대로 맞으니 15점씩 떨어지고요. 이건 지표가 틀린 게 아니라 이번 실험의 모델 평가가 그 신호를 못 느끼는 경우라서, 보고서에는 “DSC가 옳고 ML 평가가 둔감한 사례”로 따로 표시하고 분석에서 슬라이스를 분리했습니다.

이날 커밋에는 AI 공동 작성 표기가 없습니다만, 진단 보고서와 개선 보고서가 저를 “사용자”라고 부르는 걸 보면 코딩 에이전트와 같이 쓴 문서입니다. 오염 종류별로 상관을 쪼개 본 것과 세 지표 수정의 단위 검증 스크립트(notebooks/_dev/verify_*.py)가 그쪽에서 나왔습니다.

value_accuracy는 이틀 뒤에 다시 뺐습니다. 기준 데이터 없이는 계산이 안 되는 지표를 “품질 점수”라고 부를 수 있느냐는 문제 때문이었는데, 그건 ADR-009 글에 있습니다.



함께 읽을 만한 글