전처리와 탐색적 분석

먼저 읽을 장 15.1 데이터의 의미와 구조

실제 측정 데이터에는 빈칸, 튀는 값, 같은 값만 반복하는 고장 난 센서, 두 번 기록된 행, 단위가 잘못된 구간이 섞여 있다. 이런 데이터를 그대로 학습에 넣어도 코드는 오류 없이 돌아가고 loss도 줄어든다. 문제는 모델이 측정 대상이 아니라 측정 장비의 결함을 배운다는 점이고, 이는 지표만 봐서는 드러나지 않는다. 데이터를 직접 보는 시간을 아끼면 그보다 훨씬 긴 시간을 잘못된 결론을 따라가는 데 쓰게 된다.

이 장은 학습 전에 데이터에서 품질 문제를 찾고, 정규화·재표본화·보간·필터링 같은 전처리가 데이터에 무엇을 하는지 측정하고, 탐색적 분석(exploratory data analysis, EDA)으로 데이터의 구조를 확인하는 방법을 다룬다. 원칙은 두 가지다. 모든 전처리는 정보를 바꾸므로 그 영향을 측정하고 기록한다. 그리고 데이터에서 추정하는 모든 통계량은 학습 데이터에서만 추정한다. 두 번째 원칙은 15.4장의 누수 방지로 이어진다.

학습 목표

  • 결측·이상치·센서 오류를 처리한다
  • 정규화와 재표본화의 영향을 이해한다
  • 시각화로 품질을 점검한다

품질 문제

왜 필요한가. 품질 문제는 종류마다 원인과 처리 방법이 다르다. 결측을 0으로 채우는 것, 이상치를 지우는 것, 센서 멈춤을 놓치는 것은 각각 다른 방식으로 모델을 왜곡한다. 먼저 어떤 문제가 얼마나 있는지 세는 품질 점검표를 만든다.

다섯 가지 문제.

문제 증상 확인 방법 처리의 예
결측(missing) NaN, 결측 코드(15.1장) 열별·시간대별 결측 수와 비율 원인 기록 후 제외, 보간, 결측 표시 열 추가
이상치(outlier) 분포에서 크게 벗어난 값 robust z, 분위수, 직접 그림 확인 측정 오류면 결측 처리, 실제 현상이면 보존
중복(duplicate) 같은 행이 두 번 이상 키(entity, 시각) 기준 중복 검사 하나만 남기고, 왜 생겼는지 확인
센서 오류 같은 값 반복(멈춤), 물리적으로 불가능한 값, 갑작스러운 계단 연속 동일값 길이, 범위 검사, 차분의 분포 해당 구간 결측 처리, 장비 기록 대조
단위 오류 특정 출처·기간의 값이 수십~수천 배 출처·기간별 분포 비교 사전에 근거해 변환하거나 제외

1 Hz로 10분간 기록한 온도에 결측·스파이크·센서 멈춤·중복 기록을 넣고 점검표를 만든다.

import numpy as np
import pandas as pd

rng = np.random.default_rng(0)
n = 600
t_s = np.arange(n, dtype=float)                                # (600,) 1 Hz
temp_c = 25.0 + 0.002 * t_s + rng.normal(0, 0.1, n)
temp_c[rng.choice(n, 15, replace=False)] = np.nan              # 결측
temp_c[100] = 85.0                                             # 순간 스파이크
temp_c[300:340] = 25.6                                         # 센서 멈춤: 같은 값 반복
df = pd.DataFrame({"t_s": t_s, "temp_c": temp_c})
df = pd.concat([df, df.iloc[200:205]], ignore_index=True)      # 중복 기록 5행

dup = df.duplicated()
s = df[~dup].sort_values("t_s")["temp_c"]
run_len = s.groupby(s.diff().ne(0).cumsum()).size()            # 같은 값이 이어진 길이
robust_z = 0.6745 * (s - s.median()) / (s - s.median()).abs().median()
print("결측:", int(s.isna().sum()), " 중복 행:", int(dup.sum()))
print("같은 값 최장 연속:", int(run_len.max()), "샘플")
print("robust z > 5인 시각:", s.index[robust_z.abs() > 5].tolist())
결측: 13  중복 행: 5
같은 값 최장 연속: 40 샘플
robust z > 5인 시각: [100]

점검표는 결측 13개, 중복 5행, 같은 값 40번 연속(센서 멈춤), t=100t = 100 s의 스파이크를 찾았다. 센서 멈춤은 값이 정상 범위 안에 있으므로 범위 검사로는 보이지 않는다. 0.1 °C의 잡음이 있는 센서가 40초 동안 소수점 아래까지 같은 값을 낸다는 것이 물리적으로 불가능하다는 사실로 찾았다.

이상치를 어떤 기준으로 찾는가. 흔한 규칙은 평균과 표준편차로 z=(x−xˉ)/sz = (x - \bar{x})/s를 계산해 ∣z∣>3|z| > 3인 값을 이상치로 보는 것이다. 이 규칙에는 약점이 있다. 평균과 표준편차가 이상치 자신의 영향을 받는다. 이상치가 많으면 표준편차가 부풀어 이상치의 zz가 작아지고, 이상치가 스스로를 숨긴다(masking). 대안은 이상치에 덜 민감한 중앙값과 MAD(median absolute deviation)를 쓰는 robust z다.

MAD=median⁡i∣xi−median⁡(x)∣,zirobust=0.6745 xi−median⁡(x)MAD.\mathrm{MAD} = \operatorname{median}_i \lvert x_i - \operatorname{median}(x) \rvert, \qquad z^{\text{robust}}_i = 0.6745\,\frac{x_i - \operatorname{median}(x)}{\mathrm{MAD}}.

상수 0.6745는 정규분포에서 MAD≈0.6745 σ\mathrm{MAD} \approx 0.6745\,\sigma이므로, robust z를 보통의 z와 같은 척도로 맞추기 위한 것이다. 흔히 ∣zrobust∣>3.5\lvert z^{\text{robust}} \rvert > 3.5를 이상치 후보로 본다(Iglewicz와 Hoaglin이 권고한 기준으로, NIST의 공학 통계 핸드북(Engineering Statistics Handbook)에 정리되어 있다. 데이터에 맞게 조정한다).

x = np.concatenate([rng.normal(10.0, 1.0, 90), rng.uniform(40.0, 60.0, 10)])  # (100,) 정상 90 + 이상치 10
z = (x - x.mean()) / x.std()
mad = np.median(np.abs(x - np.median(x)))
robust_z = 0.6745 * (x - np.median(x)) / mad
print(f"평균 {x.mean():.1f}, 표준편차 {x.std():.1f} / 중앙값 {np.median(x):.1f}, MAD {mad:.2f}")
print("|z| > 3        :", int(np.sum(np.abs(z) > 3)), "개 검출")
print("|robust z| > 3.5:", int(np.sum(np.abs(robust_z) > 3.5)), "개 검출")
평균 13.9, 표준편차 12.0 / 중앙값 10.2, MAD 0.83
|z| > 3        : 4 개 검출
|robust z| > 3.5: 10 개 검출

이상치 10개가 표준편차를 1에서 12.0으로 부풀려, ∣z∣>3|z| > 3 규칙은 4개만 찾았다. 중앙값(10.2)과 MAD(0.83)는 거의 흔들리지 않았고 robust z는 10개를 모두 찾았다.

이상치는 지울 대상이 아니라 확인할 대상이다. 통계적 이상치가 곧 측정 오류는 아니다. 배터리의 갑작스러운 용량 저하, 단백질 실험의 드문 결합, 극한 기상은 이상치처럼 보이지만 연구가 찾는 현상일 수 있다. 이상치 후보를 찾으면 그 시점의 원 신호와 실험 기록을 직접 확인하고, 측정 오류로 판단한 근거를 처리 코드의 주석으로 남긴다(15.1장 원본 보존).

정규화와 스케일링

왜 필요한가. 전압(약 3–4 V)과 전류(수 mA–수 A), 온도(수십 °C)는 크기가 크게 다르다. 거리 기반 모델(k-최근접 이웃, 커널 방법)은 큰 값의 특징에 지배되고, 경사하강은 축마다 곡률이 크게 다른 손실 지형에서 느리게 수렴한다(10장). 그래서 특징의 크기를 비슷하게 맞춘다.

세 가지 방법. 열 jj의 값 xijx_{ij}를 다음처럼 바꾼다.

방법 식 특징
standardization (x−μ)/σ(x - \mu)/\sigma 평균 0, 표준편차 1. 이상치가 μ\mu와 σ\sigma를 끌어당긴다
min-max (x−xmin⁡)/(xmax⁡−xmin⁡)(x - x_{\min})/(x_{\max} - x_{\min}) 값을 [0,1][0, 1]에 넣는다. 이상치 하나가 나머지를 좁은 구간에 몰아넣는다
robust scaling (x−median⁡)/IQR(x - \operatorname{median})/\mathrm{IQR} 중앙값과 사분위범위(IQR)를 쓴다. 이상치에 덜 민감하다

scikit-learn의 StandardScaler, MinMaxScaler, RobustScaler가 각각에 해당한다. 셋 모두 아핀 변환이므로(7.2장) 한 집합 안의 값 순서와 상대 간격은 보존한다.

통계량을 어디서 추정하는가. 위 식의 μ\mu, σ\sigma, 중앙값, IQR은 모두 데이터에서 추정한 값이다. 모델의 가중치처럼 학습되는 파라미터다. 따라서 train에서만 추정하고, 같은 값을 validation과 test에 그대로 적용한다. 사용할수록 천천히 떨어지는 전압을 시간 순으로 train(앞 800개)과 test(뒤 200개)로 나누어 비교한다.

from sklearn.preprocessing import StandardScaler

t = np.arange(1000)
voltage_v = 3.7 - 0.0004 * t + rng.normal(0, 0.02, 1000)       # (1000,) 사용할수록 천천히 떨어짐
train, test = voltage_v[:800, None], voltage_v[800:, None]       # (800, 1), (200, 1) 시간 순 분할
scaler = StandardScaler().fit(train)                             # train에서만 fit
leaky = StandardScaler().fit(voltage_v[:, None])                 # test까지 보고 fit (누수)
print(f"train fit: 평균 {scaler.mean_[0]:.3f} V, 표준편차 {scaler.scale_[0]:.3f} V")
print(f"전체 fit : 평균 {leaky.mean_[0]:.3f} V, 표준편차 {leaky.scale_[0]:.3f} V")
print(f"test의 표준화 평균: train fit {scaler.transform(test).mean():.2f}, 전체 fit {leaky.transform(test).mean():.2f}")
train fit: 평균 3.540 V, 표준편차 0.093 V
전체 fit : 평균 3.500 V, 표준편차 0.117 V
test의 표준화 평균: train fit -2.15, 전체 fit -1.37

train에서 추정하면 test의 표준화 평균이 −2.15로, test 구간의 전압이 train보다 크게 낮다는 사실(분포 이동)이 그대로 보인다. 전체에서 추정하면 test의 낮은 값이 평균과 표준편차에 섞여 들어가 이동이 −1.37로 작아 보인다. 이것이 전처리 누수다. 모델은 test 구간에 대한 정보를 입력의 척도를 통해 미리 받았고, 평가는 실제 배포 상황(미래 데이터를 미리 볼 수 없다)보다 낙관적이 된다.

시계열 전처리

왜 필요한가. 여러 센서는 서로 다른 시각, 서로 다른 주기로 기록하고, 통신 지연으로 간격이 불규칙하다. 모델은 대개 균일한 시간 격자를 요구하므로 timestamp 정렬과 재표본화(resampling)가 필요하다. 그 과정에서 쓰는 보간·필터링은 데이터를 매끄럽게 만들지만, 매끄러워진 만큼 정보가 사라진다.

정의.

  • timestamp 정렬: 모든 센서의 시각을 같은 기준(시간대, 시계 offset)으로 맞추고 정렬한다. 장비마다 시계가 몇 초 어긋나 있으면 원인과 결과의 순서가 뒤바뀔 수 있다.
  • 재표본화: 새 시간 격자 위의 값을 정한다. 격자가 원래보다 촘촘하면 보간(upsampling), 성기면 집계나 표본 추출(downsampling)이다.
  • 보간(interpolation): 측정점 사이의 값을 추정한다. 선형 보간은 이웃한 두 측정점을 직선으로 잇는다(9.5장).
  • filtering·smoothing: 고주파 잡음을 줄인다. 길이 ww의 이동 평균은 x~t=1w∑kxt+k\tilde{x}_t = \frac{1}{w}\sum_{k} x_{t+k}로, 창 안의 값을 평균한다.

표본화 정리. 진동수 ff Hz의 성분을 표현하려면 표본화 주파수가 2f2f Hz보다 커야 한다(Nyquist 조건). 그보다 느리게 재표본화하면 빠른 성분이 느린 가짜 진동(aliasing)으로 보이거나 사라진다. 짧은 사건도 같다. 사건의 지속 시간보다 긴 간격으로 표본을 뽑으면 사건이 표본 사이에 빠질 수 있다.

30.3 s에 폭 약 1 s의 펄스(참 최댓값 1)가 있는 신호를 불규칙한 시각 200개에서 측정하고, 2 Hz로 보간한 뒤 이동 평균과 0.1 Hz 재표본화를 적용한다.

t_irr_s = np.sort(rng.uniform(0.0, 60.0, 200))                   # (200,) 불규칙한 측정 시각
pulse = np.exp(-((t_irr_s - 30.3) / 0.8) ** 2)                   # 30.3 s에 폭 약 1 s의 펄스, 참 최댓값 1
t_grid_s = np.arange(0.0, 60.0, 0.5)                             # (120,) 2 Hz 균일 격자
on_grid = np.interp(t_grid_s, t_irr_s, pulse)                    # 선형 보간
smooth = np.convolve(on_grid, np.ones(9) / 9, mode="same")       # 4.5 s 이동 평균
coarse = np.interp(np.arange(5.0, 60.0, 10.0), t_irr_s, pulse)   # 0.1 Hz로 재표본화
for name, v in [("측정점", pulse), ("2 Hz 보간", on_grid),
                ("이동 평균", smooth), ("0.1 Hz", coarse)]:
    print(f"{name:7s} 최댓값 {v.max():.3f}")
측정점     최댓값 0.870
2 Hz 보간 최댓값 0.869
이동 평균   최댓값 0.289
0.1 Hz  최댓값 0.000

네 가지를 읽는다.

  • 측정 자체가 이미 최댓값을 놓쳤다(0.870). 측정점이 정점 바로 위에 없었다.
  • 보간은 측정점 사이를 직선으로 이을 뿐(0.869) 측정에 없던 정점을 되살리지 않는다.
  • 4.5 s 이동 평균은 1 s짜리 펄스의 높이를 3분의 1로 줄였다(0.289). 창보다 짧은 현상은 잡음과 함께 평균된다.
  • 0.1 Hz 재표본화는 펄스를 완전히 지웠다(0.000). 표본 시각 5, 15, 25, 35 s 사이에 펄스가 빠졌다.

연구에서의 의미. 전처리 파라미터(보간 방법, 재표본화 주기, 창 크기)는 연구가 관심을 두는 가장 짧은 현상의 시간 척도에 맞춘다. 결정은 실험 기록에 남기고, 전처리 전후의 대표 구간을 겹쳐 그려 무엇이 사라졌는지 확인한다.

특징 추출

왜 필요한가. 원 신호를 그대로 모델에 넣을 수도 있지만, 데이터가 적은 과학 문제에서는 의미 있는 요약값(특징, feature)으로 바꾸면 적은 데이터로도 학습이 안정되고 결과를 해석하기 쉽다.

도메인 지식 기반 특징. 물리적 의미가 분명한 양을 계산한다.

  • 배터리: 충전 곡선에서 일정 전압 구간에 걸린 시간, 미분 용량 곡선(dQ/dVdQ/dV)의 정점 위치와 높이, 내부저항 추정값
  • 진동·구조물: 주파수 스펙트럼의 정점 진동수, 감쇠비
  • 분자: 분자량, 수소 결합 공여체·수용체 수 같은 기술자(descriptor)

자동 특징. 통계 요약(평균·분산·분위수·자기상관), 스펙트럼 계수, 또는 신경망이 학습한 표현(embedding)을 쓴다. 자동 특징은 사람의 가정에 덜 묶이지만, 특징이 많아지면 우연히 label과 상관된 특징이 생기기 쉽다.

특징 추출도 학습이다. 특징을 고르는 데 label을 썼다면(예: label과 상관이 큰 특징 상위 20개 선택) 그 선택도 train에서만 해야 한다. 전체 데이터로 고른 특징은 test의 label 정보를 담는다. 15.4장에서 이 누수가 순수한 잡음에서도 높은 성능을 만들어 내는 것을 확인한다. 반면 label을 쓰지 않는 고정된 계산(예: 각 샘플의 최댓값)은 어디서 계산해도 누수가 아니다. 다만 그 계산에 데이터에서 추정한 상수(정규화 통계, 임계값)가 들어 있다면 그 상수는 train에서 정한다.

탐색적 분석과 시각화

왜 필요한가. 요약 통계는 같아도 데이터는 전혀 다를 수 있다. 평균·분산·상관이 모두 같은데 그림으로 그리면 직선, 곡선, 이상치 하나에 끌려간 점들로 전혀 다른 네 데이터셋이 있다는 것은 잘 알려진 예다(Anscombe, 1973). EDA는 숫자를 계산하기 전에 데이터를 눈으로 보는 단계다.

무엇을 그리는가.

  • 분포: 열마다 히스토그램. 단위 오류(두 개의 봉우리), 결측 코드(한 값에 몰린 막대), 측정 한계(한쪽이 잘린 분포)가 보인다.
  • 상관: 특징 쌍의 산점도와 상관 행렬. label과 지나치게 강하게 상관된 특징은 누수를 의심한다.
  • 시계열 개요: entity마다 전체 기간을 한 화면에. 결측 구간, 계단, 센서 멈춤, 장비 교체 시점이 보인다.
  • 그룹별 비교: 출처·장비·배치·실험실별로 나누어 같은 그림을 그린다. 합쳐서 보면 보이지 않는 구조가 드러난다.
  • 대표 샘플 직접 보기: 무작위 샘플, label 최댓값·최솟값 근처 샘플, 이상치 후보를 원 신호로 본다.

그룹별 비교가 왜 필요한가. 장비 4대의 데이터에서 xx와 yy의 상관을 본다.

parts = []
for g, (x0, y0) in enumerate([(0, 6), (2, 4), (4, 2), (6, 0)]):  # 장비마다 기준점이 다르다
    xg = x0 + rng.normal(0, 0.7, 50)
    yg = y0 + 0.8 * (xg - x0) + rng.normal(0, 0.3, 50)
    parts.append(pd.DataFrame({"device": g, "x": xg, "y": yg}))
d = pd.concat(parts, ignore_index=True)                          # (200, 3)
print("전체 상관   :", round(d["x"].corr(d["y"]), 2))
print("장비 안 상관:", " ".join(f"{g['x'].corr(g['y']):.2f}" for _, g in d.groupby("device")))
전체 상관   : -0.84
장비 안 상관: 0.89 0.89 0.87 0.91

합치면 강한 음의 상관(−0.84)이지만, 장비 안에서는 네 장비 모두 강한 양의 상관(0.87–0.91)이다. 장비마다 기준점이 달라 장비 사이의 차이가 합친 상관을 지배했다. 집계 수준에 따라 관계의 방향이 뒤집히는 이 현상을 Simpson의 역설이라 한다. 장비 정보를 쓰지 않는 모델은 장비 차이를 xx의 효과로 오해한다. 그래서 EDA의 그림은 항상 그룹 변수로 나누어 한 번 더 그린다.

연구에서의 의미. EDA의 결과는 노트북에만 두지 않고 문서로 남긴다. 무엇을 확인했고, 무엇을 발견했고, 그 결과 어떤 전처리를 결정했는지를 그림과 함께 쓴다. 이 문서는 15.5장의 데이터 카드에서 "알려진 한계" 항목의 근거가 된다.

실습

자신의 연구 데이터(없으면 공개 시계열 데이터 하나)로 다음을 수행하고 문서로 정리한다.

  1. 품질 점검표. 열별 결측 수와 원인, 중복 수, robust z 이상치 후보 수, 같은 값 최장 연속 길이, 출처별 범위 검사 결과를 표로 만든다. 이상치 후보 중 다섯 개 이상을 원 신호로 확인하고 측정 오류인지 실제 현상인지 판단한 근거를 쓴다.
  2. 전처리 전후 비교. 적용하는 전처리(보간, 재표본화, 필터링, 스케일링)마다 전후의 분포(히스토그램)와 대표 샘플 세 개(평범한 것, 극단값, 짧은 사건이 있는 것)를 겹쳐 그린다. 각 단계에서 사라진 정보(정점 높이, 짧은 사건, 고주파 성분)를 수치로 기록한다.
  3. 그룹별 EDA. 출처·장비·배치 같은 그룹 변수로 나누어 분포와 주요 상관을 다시 계산하고, 합친 결과와 다른 점을 기록한다.

스케일링 통계량을 train에서만 추정하는 함수를 연습문제로 구현한다.

실패 사례

  • 데이터를 직접 보지 않고 바로 학습하는 것. 센서 멈춤, 결측 코드, 단위 오류가 지표를 거의 바꾸지 않은 채 모델에 들어간다. 학습 전에 품질 점검표와 그룹별 그림을 만든다.
  • 전체 데이터로 정규화 통계량을 계산하는 것. test의 정보가 입력의 척도에 섞이고, 분포 이동이 가려진다. 나눈 뒤 train에서 fit한다.
  • 이상치를 근거 없이 지우는 것. 연구가 찾는 드문 현상을 지울 수 있다. 측정 오류라는 근거를 확인하고 기록한 것만 처리한다.
  • 평균·표준편차로 이상치를 찾는 것. 이상치가 많으면 표준편차가 부풀어 이상치가 숨는다. 중앙값·MAD를 쓴다.
  • smoothing 창을 현상의 시간 척도보다 길게 잡는 것. 짧은 사건의 높이와 모양이 사라진다. 창 크기의 근거를 기록하고 전후를 겹쳐 그린다.
  • 결측을 0으로 채우는 것. 0이 물리적으로 가능한 값이면 결측이 측정값처럼 보인다. 보간하거나, 결측 표시 열을 함께 넣는다.

통과 기준

  • 품질 점검표와 전처리 단계별 전후 비교 그림을 만들고, 각 단계의 근거와 잃은 정보를 수치로 기록한다.
  • 모든 데이터 기반 통계량(스케일링, 특징 선택, 임계값)이 train에서만 추정됨을 코드로 보인다.
  • 연습문제 train-only-standardize의 테스트를 통과한다.

참고 문헌

  • Anscombe (1973). Graphs in statistical analysis. The American Statistician, 27(1), 17–21. — 요약 통계가 같아도 그림이 전혀 다른 네 데이터셋