데이터 전처리에서 중요한 작업 중 하나가 변수의 표현 방식을 분석 목적에 맞게 바꾸는 것이다.
원본 데이터가 그대로도 의미가 있을 수 있지만, 분석이나 머신러닝에 바로 사용하기에는 지나치게 세밀하거나 변수마다 값의 범위가 너무 다를 때가 있다.
예를 들어 웹 서비스 데이터를 생각해보면 다음과 같은 문제가 생길 수 있다.
응답 시간(ms): 80 ~ 3,000
요청 크기(KB): 1 ~ 500
재시도 횟수: 0 ~ 5
이 값들을 그대로 비교하면 단위와 범위가 서로 다르기 때문에 해석이나 모델 학습에서 특정 변수가 지나치게 큰 영향을 줄 수 있다.
또 사용자 활동량처럼 연속형 값을 그대로 보는 것보다 낮음, 보통, 높음처럼 묶는 것이 더 이해하기 쉬운 경우도 있다.
이럴 때 사용할 수 있는 대표적인 데이터 변환 방법이 구간화(Binning) 와 스케일링(Scaling) 이다.
원본 연속형 데이터
↓
목적에 맞는 변환
├─ 구간화 → 범주형 정보로 단순화
└─ 스케일링 → 값의 범위를 조정
이번 글에서는 Pandas와 scikit-learn을 이용해 구간화와 Min-Max Scaling, Z-Score 기반 표준화를 정리한다.
데이터 변환은 원본 값을 무조건 바꾸는 작업이 아니라 분석 목적에 더 잘 맞는 형태로 표현을 바꾸는 과정이다.
대표적인 목적은 다음과 같다.
같은 숫자라도 문제에 따라 의미가 다르기 때문에 변환 기준은 항상 목적과 도메인 지식을 함께 고려해야 한다.
구간화는 연속형 데이터를 일정한 범위로 나누고 각 구간에 범주를 부여하는 방법이다.
예를 들어 API 호출 횟수가 다음과 같다고 하자.
12, 35, 80, 120, 310, 760
이를 그대로 사용하는 대신 다음처럼 단순화할 수 있다.
0 ~ 100 → low
100 ~ 500 → medium
500 이상 → high
이렇게 하면 숫자 하나하나를 비교하는 대신 사용량 수준이라는 새로운 의미를 가진 변수를 만들 수 있다.
구간화는 크게 두 가지 방식으로 접근할 수 있다.
| 방식 | 기준 | Pandas |
|---|---|---|
| 구간 범위 직접 지정 | 사용자가 경계값을 직접 정함 | pd.cut() |
| 분포 기반 분할 | 각 구간에 비슷한 수의 데이터를 배치 | pd.qcut() |
둘 중 어떤 방식이 더 좋다고 단정할 수는 없다.
서비스 정책처럼 명확한 기준이 있다면 cut()이 자연스럽고, 데이터의 상대적인 순위를 기준으로 비슷한 크기의 그룹을 만들고 싶다면 qcut()이 유용하다.
pd.cut()으로 원하는 기준 직접 만들기웹 서비스의 일일 요청 수를 기준으로 사용량을 분류한다고 가정해보자.
import numpy as np
import pandas as pd
df = pd.DataFrame({
"request_count": [72, 95, 130, 250, 480, 650, 780]
})
구간 경계를 직접 지정한다.
bins = [0, 100, 500, np.inf]
labels = ["low", "medium", "high"]
df["usage_level"] = pd.cut(
df["request_count"],
bins=bins,
labels=labels
)
결과를 확인한다.
print(df)
핵심은 구간의 숫자 자체보다 왜 그 경계를 선택했는지다.
예를 들어 실제 서비스라면 다음과 같은 기준을 사용할 수 있다.
즉, pd.cut()은 비즈니스나 도메인에서 이미 의미가 있는 경계값을 표현하기 좋다.
80개 관측값을 세 구간으로 나누는 실습에서는 다음 경계를 사용했다.
bins = [0, 100, 500, max_value]
세 범주로 나눈 결과는 다음과 같았다.
medium 73
low 5
high 2
데이터 대부분이 medium에 몰려 있고 high는 2개뿐이었다.
이 결과는 cut()의 중요한 특징을 보여준다.
구간의 폭을 사용자가 정한다고 해서 각 구간에 데이터가 비슷한 개수로 들어가는 것은 아니다.
pd.qcut()으로 분포 기준 구간 만들기각 그룹에 가능한 한 비슷한 개수의 데이터를 넣고 싶다면 qcut()을 사용할 수 있다.
labels = ["low", "medium", "high"]
df["usage_level_q"] = pd.qcut(
df["request_count"],
q=3,
labels=labels
)
q=3은 데이터를 분위수 기준으로 세 그룹으로 나누겠다는 뜻이다.
개념적으로는 다음과 같다.
전체 데이터 정렬
↓
관측값 개수가 비슷하도록 분할
↓
low / medium / high
80개 데이터를 세 구간으로 나눈 결과는 다음과 같았다.
low 29
medium 26
high 25
cut() 결과와 비교하면 차이가 분명하다.
| 방식 | low | medium | high |
|---|---|---|---|
| 직접 경계 지정 | 5 | 73 | 2 |
| 분포 기반 구간화 | 29 | 26 | 25 |
qcut()은 값의 간격보다 데이터 개수의 균형을 더 중요하게 본다.
qcut()에서 주의할 점데이터에 같은 값이 지나치게 많이 반복되면 분위수 경계를 만들기 어려울 수 있다.
예를 들어 다음 데이터가 있다고 하자.
1, 1, 1, 1, 1, 3, 8, 10
동일한 값이 여러 개 몰려 있으므로 원하는 개수의 구간으로 정확히 분리하기 어려울 수 있다.
이런 경우에는 다음 방법을 검토할 수 있다.
pd.cut()으로 도메인 기준 구간 사용구간화는 단순한 함수 호출보다 데이터 분포를 먼저 보는 것이 중요하다.
구간화가 값을 범주로 바꾸는 작업이라면, 스케일링은 연속형 값을 유지하면서 값의 크기 범위를 조정하는 작업이다.
예를 들어 머신러닝 입력 변수가 다음과 같다고 하자.
page_view 1 ~ 100
session_time 10 ~ 10,000
purchase_count 0 ~ 8
숫자 크기만 보면 session_time이 훨씬 크다.
특히 거리(distance)를 사용하는 알고리즘에서는 이런 차이가 결과에 영향을 줄 수 있다.
대표적으로 K-Means는 관측값 사이의 거리를 기반으로 비슷한 데이터를 묶는다.
서로 다른 변수 범위
↓
거리 계산에서 특정 변수 영향이 커질 수 있음
↓
스케일 조정
↓
변수 간 비교 가능한 수준으로 맞춤
Min-Max Scaling은 데이터의 최솟값을 0, 최댓값을 1로 맞추고 나머지 값을 그 사이에 배치한다.
공식은 다음과 같다.
x_scaled = (x - min) / (max - min)
scikit-learn에서는 MinMaxScaler를 사용할 수 있다.
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
df["scaled_value"] = scaler.fit_transform(
df[["request_count"]]
)
변환 후 확인한다.
print(df[["request_count", "scaled_value"]])
describe()로 범위를 확인하면 된다.
df["scaled_value"].describe()
정상적으로 변환되었다면 기본 설정에서는 다음 특징을 가진다.
min → 0
max → 1
80개 데이터에 Min-Max Scaling을 적용했을 때 실제로 다음 범위를 확인했다.
원본 최소값: 5
원본 최대값: 780
변환 후 최소값: 0.0
변환 후 최대값: 1.0
또 원본 값 650은 약 0.832258로 변환되었다.
즉, Min-Max Scaling은 원래 값 자체보다 전체 범위 안에서 어느 위치에 있는 값인지 보기 쉽게 만든다.
Z-Score 방식은 각 값이 평균에서 표준편차 몇 배만큼 떨어져 있는지를 나타낸다.
z = (x - 평균) / 표준편차
scikit-learn에서는 StandardScaler를 사용한다.
from sklearn.preprocessing import StandardScaler
std_scaler = StandardScaler()
df["standardized_value"] = std_scaler.fit_transform(
df[["request_count"]]
)
이 방식은 데이터를 평균 0을 중심으로 재배치하고 표준편차를 기준으로 스케일을 조정한다.
용어는 자료나 라이브러리 문맥에 따라 조금씩 다르게 쓰인다. 실무에서는
StandardScaler를 보통 표준화(Standardization) 라고 부르고, Min-Max Scaling과 함께 넓은 의미의 스케일링 방법으로 다루는 경우가 많다.
실습 데이터에 StandardScaler를 적용한 결과 전체 평균은 거의 0에 가까워졌다.
mean ≈ 0
또 최대값에 해당하는 관측치는 약 다음 값으로 변환되었다.
max z-score ≈ 6.640282
중간에 있던 큰 값 하나는 다음과 같이 확인되었다.
원본 값: 650
표준화 값: 약 5.265510
이처럼 표준화된 값은 평균에서 얼마나 멀리 떨어져 있는지 해석하기 좋다.
두 방식은 모두 변수의 스케일을 조정하지만 해석 방식이 다르다.
| 구분 | Min-Max Scaling | Z-Score 표준화 |
|---|---|---|
| 대표 클래스 | MinMaxScaler | StandardScaler |
| 중심 개념 | 최소~최대 범위 내 위치 | 평균으로부터의 거리 |
| 기본 범위 | 보통 0~1 | 고정된 상·하한 없음 |
| 변환 후 중심 | 데이터에 따라 다름 | 평균이 0에 가까움 |
| 극단값 영향 | 범위를 크게 늘릴 수 있음 | 평균·표준편차에도 영향 |
| 활용 관점 | 정해진 범위로 맞추고 싶을 때 | 서로 다른 단위의 변수를 비교할 때 |
어느 방식이 항상 더 좋은 것은 아니다.
데이터 분포와 사용할 알고리즘에 따라 선택해야 한다.
실습 데이터에는 최댓값이 780인 큰 값이 포함되어 있었다.
원본 통계는 다음과 같았다.
count 80
mean 152.0875
std 95.1577
min 5
median 141
max 780
Min-Max Scaling을 적용하면 이 최댓값이 1이 되고 나머지 값들은 0~1 사이에 압축된다.
StandardScaler를 적용하면 이 값의 Z-Score가 약 6.64가 된다.
이 결과에서 기억할 점은 다음과 같다.
스케일링은 이상치를 제거하는 작업이 아니다.
이상치 포함 데이터
↓
Scaling
↓
이상치가 사라지는 것이 아니라
변환된 공간에서도 영향이 남음
따라서 머신러닝용 전처리에서는 필요하다면 이상치 처리 여부를 먼저 검토한 뒤 스케일링을 적용해야 한다.
fit()과 transform()을 분리해서 이해하기scikit-learn의 Scaler를 처음 사용할 때는 fit_transform()만 외우기 쉽다.
하지만 실제 모델링에서는 둘의 역할을 구분해서 이해하는 것이 중요하다.
scaler.fit(train_df[["request_count"]])
train_scaled = scaler.transform(
train_df[["request_count"]]
)
test_scaled = scaler.transform(
test_df[["request_count"]]
)
개념적으로는 다음과 같다.
fit
→ 변환에 필요한 기준값 학습
(min, max, mean, std 등)
transform
→ 학습한 기준으로 실제 데이터 변환
훈련 데이터와 테스트 데이터에 각각 따로 fit()하면 데이터 누수로 이어질 수 있다.
따라서 머신러닝에서는 훈련 데이터로 기준을 학습하고, 테스트 데이터에는 동일한 기준으로 transform만 수행하는 패턴을 기억하는 것이 좋다.
둘 다 데이터 변환이지만 결과의 의미는 완전히 다르다.
| 구분 | 구간화 | 스케일링 |
|---|---|---|
| 입력 | 연속형 값 | 연속형 값 |
| 출력 | 범주형 값 | 연속형 값 |
| 목적 | 의미 있는 그룹으로 단순화 | 값의 범위 조정 |
| 대표 함수 | cut, qcut | MinMaxScaler, StandardScaler |
| 예시 | 트래픽을 low/medium/high로 분류 | 요청 수를 0~1 범위로 변환 |
구간화는 정보의 세밀함을 일부 포기하는 대신 해석이 쉬워지고, 스케일링은 원래의 순서 관계를 유지하면서 값의 표현 범위를 바꾼다.
웹 서비스의 사용자 데이터를 예로 들어보자.
다음과 같은 특성이 있다고 가정한다.
login_count
session_minutes
purchase_amount
api_request_count
사용자 활동 등급을 만들고 싶다면:
api_request_count
↓
low / medium / high
이 결과는 리포트, 대시보드, 운영 기준처럼 사람이 읽는 영역에서 활용하기 쉽다.
여러 수치형 변수를 머신러닝 입력으로 사용한다면:
login_count 0 ~ 30
session_minutes 1 ~ 5,000
purchase_amount 0 ~ 3,000,000
그대로 거리 기반 모델에 넣기보다 스케일을 조정하는 것이 중요할 수 있다.
결국 변환 방법은 다음 질문에서 출발하면 된다.
이 값을 사람이 해석하기 쉽게 묶고 싶은가?
→ 구간화
수치 정보는 유지하면서 변수 범위를 맞추고 싶은가?
→ 스케일링
데이터 변환 코드를 실행한 뒤에는 변환 자체가 성공했는지만 보지 말고 결과의 의미도 확인해야 한다.
구간화 후 각 범주의 개수 확인
df["category"].value_counts()
경계 근처 값이 의도한 범주에 들어갔는지 확인
df[["value", "category"]].head()
Min-Max Scaling 후 최솟값과 최댓값 확인
df["scaled_value"].describe()
StandardScaler 후 평균이 0에 가까운지 확인
df["standardized_value"].mean()
극단값이 변환 결과에 어떤 영향을 주는지 확인
머신러닝에서는 train 데이터만으로 Scaler를 fit했는지 확인
pd.cut()은 사용자가 직접 구간 경계를 정할 때 사용한다.pd.qcut()은 관측값 개수가 비슷하도록 분위수 기준으로 구간을 나눈다.qcut()이 원하는 구간을 만들기 어려울 수 있다.fit()하고 테스트 데이터에는 transform()만 적용하는 것이 중요하다.구간화와 스케일링은 모두 데이터를 바꾸는 작업이지만 목적은 다르다.
구간화는 수치형 데이터를 의미 있는 그룹으로 단순화하는 데 초점이 있고, 스케일링은 수치 정보 자체는 유지하면서 변수 사이의 크기 차이를 조정하는 데 초점이 있다.
중요한 것은 함수를 외우는 것보다 왜 변환하는지 먼저 정의하는 것이다.
분석 목적 확인
↓
데이터 분포 확인
↓
구간화 또는 스케일링 선택
↓
변환 결과 검증
↓
분석 / 모델링 적용
전처리에서 데이터 변환은 단순한 형식 변경이 아니라 이후 분석과 모델이 데이터를 바라보는 기준을 만드는 과정이라고 볼 수 있다.
#Python #Pandas #DataPreprocessing #데이터전처리 #데이터변환 #Binning #구간화 #Normalization #Standardization #ScikitLearn #MachineLearning #DataScience