데이터 전처리에서 특징 생성 이해하기: One-Hot Encoding부터 PCA·K-Means까지

Alchemist·약 18시간 전

KT AIVLE

목록 보기
11/14

데이터 분석이나 머신러닝을 시작하면 처음에는 원본 데이터를 깨끗하게 만드는 것에 집중하게 된다. 결측치를 처리하고, 이상치를 확인하고, 스케일을 맞추는 식이다.

그런데 전처리는 단순히 데이터를 정리하는 데서 끝나지 않는다. 원본 변수에서 분석에 더 유용한 정보를 뽑아내거나, 여러 변수를 새로운 형태로 표현하는 특징 생성(Feature Engineering) 도 중요한 과정이다.

예를 들어 서버 로그에 다음과 같은 값이 있다고 생각해보자.

접속 시각
디바이스 종류
요청 수
오류 수
평균 응답 시간

이 데이터를 그대로 사용할 수도 있지만, 실제 분석에서는 아래와 같은 정보가 더 유용할 수 있다.

접속 시각
    ↓
시간대 / 요일 / 주말 여부

요청 수 + 오류 수
    ↓
오류율

여러 성능 지표
    ↓
PCA로 압축된 주성분

여러 사용자 행동 지표
    ↓
K-Means 군집 번호

즉, 특징 생성은 단순히 컬럼 수를 늘리는 작업이 아니라 문제를 더 잘 설명할 수 있는 표현을 만드는 작업이라고 볼 수 있다.


특징 생성이 필요한 이유

좋은 모델을 사용해도 입력 데이터가 문제를 제대로 설명하지 못하면 좋은 결과를 얻기 어렵다.

원본 데이터에는 다음과 같은 한계가 있을 수 있다.

  • 문자열 범주를 모델이 직접 처리하기 어려움
  • 날짜 하나에 여러 정보가 섞여 있음
  • 변수 하나보다 변수 간 관계가 더 중요한 경우가 있음
  • 변수 수가 너무 많아 계산량이 커짐
  • 서로 비슷한 정보를 가진 변수가 많음

이런 문제를 해결하기 위해 특징을 새롭게 표현한다.

Raw Data
    ↓
의미 있는 정보 추출
    ↓
Feature Engineering
    ↓
분석 / 머신러닝 모델

특징 생성에서 중요한 점은 새로운 컬럼을 많이 만드는 것 자체가 목적이 아니라는 것이다. 분석 목적에 실제로 도움이 되는 정보인지를 계속 확인해야 한다.


1. 범주형 데이터는 숫자로 표현해야 한다

머신러닝 알고리즘 중 많은 경우 입력값으로 수치형 데이터를 사용한다.

하지만 실제 서비스 데이터에는 문자열 범주가 자주 등장한다.

device
- desktop
- mobile
- tablet

이 값을 단순히 다음처럼 바꾸는 방법도 생각할 수 있다.

desktop → 1
mobile  → 2
tablet  → 3

하지만 이 방식은 실제로 존재하지 않는 순서 관계를 모델에 전달할 수 있다.

tablet = 3이 desktop = 1보다 세 배 큰 의미를 갖는 것은 아니기 때문이다.

순서가 없는 범주형 데이터에서는 One-Hot Encoding을 자주 사용한다.


One-Hot Encoding

One-Hot Encoding은 각 범주를 별도의 컬럼으로 만들고 해당 범주 여부를 0 또는 1로 표현한다.

device
mobile
desktop
mobile

        ↓

device_desktop    device_mobile
0                 1
1                 0
0                 1

Pandas에서는 pd.get_dummies()로 간단하게 처리할 수 있다.

import pandas as pd

events = pd.DataFrame({
    "device": ["mobile", "desktop", "tablet", "mobile"],
    "request_count": [120, 85, 40, 210]
})

encoded = pd.get_dummies(
    events,
    columns=["device"],
    dtype=int
)

print(encoded)

여기서 확인할 것은 원래 device라는 문자열 컬럼 대신 범주별 컬럼이 만들어졌다는 점이다.

언제 유용할까?

명목형(Nominal) 데이터, 즉 범주 사이에 순서가 없는 데이터를 모델 입력으로 사용할 때 적합하다.

예를 들면 다음과 같다.

  • 브라우저 종류
  • 디바이스 종류
  • 결제 수단
  • 유입 채널
  • 상품 카테고리

반대로 초급 → 중급 → 고급처럼 실제 순서가 있는 값은 별도의 인코딩 방식을 검토하는 편이 좋다.


범주가 너무 많다면?

One-Hot Encoding에는 단점도 있다.

범주가 5개라면 컬럼 몇 개만 추가되지만, 사용자 ID처럼 값이 수십만 개라면 새로운 컬럼도 그만큼 늘어날 수 있다.

이런 현상을 고차원화라고 볼 수 있다.

따라서 One-Hot Encoding을 사용하기 전에는 아래를 확인하는 것이 좋다.

events["device"].nunique()

범주의 개수(Cardinality) 가 지나치게 많다면 One-Hot Encoding을 무조건 적용하기보다 다른 표현 방법을 검토해야 한다.


2. 하나의 변수 안에 숨어 있는 정보를 분해하기

특징 생성에서 자주 사용하는 방법이 분해(Decomposition) 다.

대표적인 예가 날짜와 시간이다.

2026-09-24 14:35:00

사람에게는 하나의 날짜처럼 보이지만 분석에서는 여러 정보를 얻을 수 있다.

연도
월
일
요일
시간
주말 여부
시간대

먼저 문자열로 저장된 날짜를 datetime으로 변환한다.

logs = pd.DataFrame({
    "event_time": [
        "2026-09-21 08:30",
        "2026-09-21 14:10",
        "2026-09-26 22:40"
    ]
})

logs["event_time"] = pd.to_datetime(logs["event_time"])

info()로 확인하면 날짜 컬럼이 단순 object가 아니라 datetime64 계열로 변환됐는지 확인할 수 있다.

logs.info()

이제 .dt 접근자를 이용해 필요한 정보를 분리할 수 있다.

logs["year"] = logs["event_time"].dt.year
logs["month"] = logs["event_time"].dt.month
logs["day"] = logs["event_time"].dt.day
logs["hour"] = logs["event_time"].dt.hour
logs["dayofweek"] = logs["event_time"].dt.dayofweek

dayofweek는 기본적으로 다음처럼 표현된다.

월요일 → 0
화요일 → 1
...
일요일 → 6

이를 다시 서비스 관점에서 의미 있는 특징으로 바꿀 수도 있다.

logs["is_weekend"] = logs["dayofweek"].isin([5, 6]).astype(int)

시간 역시 단순 숫자보다 시간대라는 의미가 더 적합한 경우가 있다.

logs["time_band"] = pd.cut(
    logs["hour"],
    bins=[-1, 5, 11, 17, 23],
    labels=["dawn", "morning", "afternoon", "evening"]
)

이렇게 하면 하나의 날짜 컬럼에서 여러 파생 변수를 만들 수 있다.

event_time
    ↓
year
month
day
hour
dayofweek
is_weekend
time_band

3. 여러 변수를 결합해 새로운 의미 만들기

분해와 반대로 기존 변수 여러 개를 결합(Combination) 해서 새로운 특징을 만들 수도 있다.

예를 들어 다음 두 값이 있다고 하자.

request_count
error_count

오류 개수만 보면 사용량이 많은 서버가 항상 나빠 보일 수 있다.

하지만 실제로는 오류율이 더 의미 있는 지표일 수 있다.

service = pd.DataFrame({
    "request_count": [1000, 500, 2000],
    "error_count": [20, 15, 10],
    "latency_ms": [120, 180, 95]
})

service["error_rate"] = (
    service["error_count"]
    / service["request_count"]
)

이처럼 나눗셈을 활용하면 데이터 크기의 영향을 줄인 비율 특징을 만들 수 있다.

다른 결합 예시

# 요청량과 응답시간이 동시에 증가하는 상황을 표현
service["traffic_pressure"] = (
    service["request_count"]
    * service["latency_ms"]
)

# 성공 요청 수
service["success_count"] = (
    service["request_count"]
    - service["error_count"]
)

단순한 사칙연산이지만 새로 만든 값이 문제를 더 직접적으로 설명한다면 좋은 특징이 될 수 있다.


Interaction Feature란?

두 변수의 관계 자체가 의미가 있을 때 곱셈 등으로 만든 특징을 Interaction Feature라고 부르기도 한다.

예를 들어 API 서버에서는 CPU 사용률이 높다는 사실 하나보다 다음 상황이 더 중요할 수 있다.

CPU 사용률 높음
+
요청 수 많음

이를 하나의 특징으로 표현해보면 다음과 같다.

service["load_interaction"] = (
    service["cpu_usage"]
    * service["request_count"]
)

다만 아무 변수나 무작정 곱한다고 좋은 특징이 되는 것은 아니다.

도메인 지식과 문제 정의를 바탕으로 왜 이 관계가 필요한지 설명할 수 있어야 한다.


4. 특징을 만들 때 Target Leakage를 조심하자

파생 변수를 만들다 보면 모델이 예측해야 하는 결과를 간접적으로 포함하는 실수가 생길 수 있다.

예를 들어 회원 탈퇴 여부를 예측하면서 탈퇴 이후에만 생성되는 값을 입력 변수로 사용하면 모델은 실제 예측이 아니라 정답을 미리 보는 것과 비슷해진다.

이것을 Target Leakage라고 한다.

특징을 만들 때는 다음 질문을 확인하는 습관이 중요하다.

이 값은 실제 예측 시점에도 알 수 있는 정보인가?

예측 시점에는 존재하지 않는 데이터라면 입력 특징으로 사용하면 안 된다.


5. 너무 많은 변수를 줄이는 PCA

특징 생성이라고 하면 새로운 컬럼을 추가하는 것만 떠올리기 쉽지만, 반대로 여러 변수의 정보를 압축해 더 적은 수의 새로운 특징을 만들 수도 있다.

대표적인 방법이 PCA(Principal Component Analysis, 주성분 분석) 다.

PCA는 서로 연관된 여러 수치형 변수의 정보를 새로운 축으로 재구성한다.

feature_1 ─┐
feature_2 ─┤
feature_3 ─┤
...        ├─→ PCA → PC1, PC2, ...
feature_n ─┘

새로 생성된 PC1, PC2 같은 값이 주성분(Principal Component) 이다.


PCA 전에 Scaling이 중요한 이유

PCA는 변수의 분산을 기반으로 새로운 축을 찾기 때문에 변수마다 스케일이 크게 다르면 값이 큰 변수가 결과를 과도하게 좌우할 수 있다.

따라서 일반적으로 PCA 전에 표준화(Standardization) 를 수행한다.

from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

features = [
    "request_count",
    "latency_ms",
    "cpu_usage",
    "memory_usage",
    "response_size"
]

X = service_df[features]

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

그 다음 PCA를 적용한다.

pca = PCA(n_components=2)

X_pca = pca.fit_transform(X_scaled)

pca_df = pd.DataFrame(
    X_pca,
    columns=["pc1", "pc2"]
)

핵심은 원래 여러 개였던 변수가 pc1, pc2라는 새로운 두 특징으로 표현된다는 점이다.


실제 실행에서 확인한 차원 변화

수치형 입력 데이터에 PCA를 적용하는 실습에서는 입력 크기가 다음과 같았다.

입력 변수: 30개
관측값: 569개

두 개의 주성분만 유지하도록 설정했을 때 결과는 다음 형태가 되었다.

(569, 30)
    ↓ PCA
(569, 2)

즉, 관측값의 개수는 그대로 유지하면서 특징의 수만 30개에서 2개로 줄어든 것을 확인할 수 있었다.


6. PCA의 n_components는 어떻게 정할까?

주성분의 개수를 무조건 직접 지정할 필요는 없다.

PCA(n_components=2)

처럼 개수를 지정할 수도 있지만, 전체 데이터 분산을 어느 정도 설명할지를 기준으로 설정할 수도 있다.

pca = PCA(n_components=0.8)

X_pca = pca.fit_transform(X_scaled)

0.8은 전체 분산을 최소 80% 이상 설명할 수 있을 만큼의 주성분을 자동으로 선택하겠다는 의미다.

제공된 실행 결과에서는 30개 입력 변수를 대상으로 이 방식을 적용했을 때 5개의 주성분이 생성되는 것을 확인했다.

30 features
    ↓ PCA(n_components=0.8)
5 principal components

데이터에 따라 필요한 주성분 개수는 달라진다.

실제로 얼마나 설명하는지는 다음과 같이 확인할 수 있다.

print(pca.explained_variance_ratio_)
print(pca.explained_variance_ratio_.sum())

7. PCA 결과를 시각화해보기

두 개의 주성분으로 축소했다면 산점도로 데이터를 표현하기 쉬워진다.

시각화를 위해 seaborn을 사용할 수 있다.

Jupyter Notebook 환경이라면 다음처럼 설치할 수 있다.

%pip install seaborn

터미널에서는 다음 명령을 사용할 수 있다.

python -m pip install seaborn

실행 기록에서는 Requirement already satisfied가 출력되어 이미 현재 Python 환경에 설치되어 있음을 확인할 수 있었다.

설치가 끝났다면 다음처럼 두 주성분을 그릴 수 있다.

import matplotlib.pyplot as plt
import seaborn as sns

sns.scatterplot(
    data=pca_df,
    x="pc1",
    y="pc2"
)

plt.show()

분류 라벨이 있다면 색을 구분해 데이터가 어느 정도 분리되는지도 확인할 수 있다.

sns.scatterplot(
    data=pca_df,
    x="pc1",
    y="pc2",
    hue="target"
)

plt.show()

여기서 시각화는 모델 성능을 증명하는 것이 아니라 축소된 특징 공간에서 데이터 구조를 탐색하는 용도로 보는 것이 좋다.


8. 군집 결과도 하나의 특징이 될 수 있다

차원을 줄이는 또 다른 아이디어는 여러 변수로부터 군집(Cluster) 을 만들고 그 군집 번호를 새로운 특징으로 사용하는 것이다.

예를 들어 사용자 행동 데이터가 다음과 같다고 하자.

page_view
session_minutes
search_count
cart_count
purchase_count

이 여러 값을 기반으로 K-Means를 수행하면 사용자별로 하나의 군집 번호를 얻을 수 있다.

여러 행동 변수
    ↓
K-Means
    ↓
cluster = 0, 1, 2, ...

이 군집 번호를 이후 모델의 새로운 특징으로 사용할 수 있다.


K-Means 기반 특징 생성

K-Means 역시 거리 기반 알고리즘이기 때문에 변수 스케일을 맞추는 것이 중요하다.

from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans

features = [
    "page_view",
    "session_minutes",
    "search_count",
    "cart_count",
    "purchase_count"
]

X = users[features]

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

군집 개수를 정하고 모델을 생성한다.

kmeans = KMeans(
    n_clusters=5,
    random_state=10,
    n_init=10
)

users["behavior_cluster"] = kmeans.fit_predict(X_scaled)

이제 여러 개의 행동 변수에서 behavior_cluster라는 하나의 새로운 특징이 생긴다.

users[
    ["page_view", "session_minutes", "behavior_cluster"]
].head()

군집 번호는 순서가 아니다

K-Means를 사용할 때 특히 주의할 점이 있다.

cluster 0
cluster 1
cluster 2

이 숫자는 단순한 그룹 식별자다.

cluster 2가 cluster 1보다 크거나 우수하다는 뜻이 아니다.

따라서 모델에 따라서는 이 군집 번호를 다시 범주형 특징으로 처리하는 것도 검토할 수 있다.


9. PCA와 K-Means 특징 생성의 차이

두 방법 모두 여러 수치형 변수를 새로운 특징으로 압축할 수 있지만 의미는 다르다.

구분PCAK-Means 기반 특징
핵심 목적분산 정보를 보존하며 새로운 축 생성비슷한 관측값을 그룹화
결과연속형 주성분군집 라벨
대표 결과pc1, pc2cluster
Scaling일반적으로 중요거리 기반이므로 중요
활용차원 축소, 시각화, 모델 입력사용자 세그먼트, 추가 특징
해석원본 변수보다 직관성이 낮을 수 있음각 군집 특성을 별도로 분석 가능

PCA는 원래 변수의 정보를 여러 연속적인 축으로 압축한다.

반면 K-Means는 관측값을 비슷한 그룹으로 묶어 그룹 소속 자체를 새로운 특징으로 만든다.


10. Feature Engineering과 Feature Selection은 다르다

헷갈리기 쉬운 개념이 특징 생성과 특징 선택이다.

Feature Engineering / Feature Creation

기존 데이터를 변환해 새로운 특징을 만든다.

timestamp → hour, weekday
request / error → error_rate
여러 변수 → PCA
여러 행동 변수 → cluster

Feature Selection

이미 존재하는 특징 중 필요한 것만 남긴다.

feature_1
feature_2
feature_3
feature_4

    ↓ 선택

feature_1
feature_3

두 방법 모두 모델 입력을 개선하기 위한 방법이지만 접근 방법은 다르다.


11. 학습 데이터와 테스트 데이터에서 주의할 점

PCA, StandardScaler, K-Means처럼 데이터에서 기준을 학습하는 변환기는 전체 데이터에 먼저 fit하면 안 된다.

머신러닝에서는 일반적으로 다음 흐름을 사용한다.

Train Data
    ↓
Scaler.fit()
PCA.fit()
KMeans.fit()
    ↓
기준 학습

Test Data
    ↓
이미 학습한 객체로 transform / predict

예를 들어 PCA라면 다음과 같다.

scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)

pca.fit(X_train_scaled)

X_train_pca = pca.transform(X_train_scaled)
X_test_pca = pca.transform(X_test_scaled)

테스트 데이터까지 포함해 fit()하면 Data Leakage가 발생할 수 있기 때문이다.

실제 프로젝트에서는 Pipeline을 사용하는 것도 좋은 방법이다.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("pca", PCA(n_components=0.8))
])

개발자 관점에서 특징 생성을 생각해보기

웹 서비스에서는 이미 많은 데이터를 수집하고 있지만, 수집된 값 자체보다 그 값을 어떤 의미로 재구성할지가 더 중요할 때가 많다.

예를 들어 사용자 행동 로그가 있다고 하자.

login_at
device
page_view
search_count
cart_count
purchase_count

이를 다음처럼 바꿀 수 있다.

login_at
    → hour
    → weekday
    → is_weekend

device
    → One-Hot Encoding

page_view + purchase_count
    → conversion-related feature

search_count + cart_count
    → interaction feature

행동 지표 전체
    → behavior_cluster

원본 로그는 단순한 기록이지만, 특징 생성 이후에는 사용자의 행동 패턴을 설명하는 데이터가 된다.

결국 Feature Engineering은 다음 질문을 계속 던지는 과정에 가깝다.

현재 컬럼만으로 내가 풀려는 문제를 충분히 설명할 수 있는가?


특징 생성 실습에서 확인할 체크포인트

코드가 에러 없이 실행됐다고 해서 특징 생성이 끝난 것은 아니다.

다음 항목을 함께 확인하는 것이 좋다.

  1. 범주형 변환

    • 범주가 의도한 개수만큼 생성됐는가?
    • 순서가 없는 값을 숫자 순서로 잘못 표현하지 않았는가?
    • 범주 수가 지나치게 많지는 않은가?
  2. 날짜 파생 변수

    • 먼저 datetime 타입으로 변환됐는가?
    • 요일과 시간대 기준이 서비스 정의와 맞는가?
    • 주말이나 영업시간 같은 도메인 규칙을 정확히 반영했는가?
  3. 결합 특징

    • 새 특징이 실제 의미를 가지고 있는가?
    • 0으로 나누는 경우가 없는가?
    • Target Leakage가 발생하지 않는가?
  4. PCA

    • Scaling 후 적용했는가?
    • 변환 전후 shape이 어떻게 달라졌는가?
    • 설명 분산 비율이 충분한가?
  5. K-Means

    • Scaling을 적용했는가?
    • 군집 수 k가 합리적인가?
    • 군집 번호 자체에 순서 의미를 부여하지 않았는가?

핵심 정리

  • Feature Engineering은 원본 데이터를 문제 해결에 더 적합한 특징으로 재구성하는 과정이다.
  • 순서가 없는 범주형 변수는 One-Hot Encoding으로 수치형 표현을 만들 수 있다.
  • Pandas의 pd.get_dummies()를 이용하면 One-Hot Encoding을 쉽게 적용할 수 있다.
  • 날짜 데이터는 pd.to_datetime()으로 변환한 뒤 연도, 월, 요일, 시간 등의 특징으로 분해할 수 있다.
  • 변수의 합, 차, 비율, 곱을 이용해 기존 변수보다 의미 있는 파생 변수를 만들 수 있다.
  • PCA는 여러 수치형 변수의 정보를 주성분으로 압축해 차원을 줄이는 방법이다.
  • PCA 전에는 변수 간 스케일 차이를 줄이기 위해 StandardScaler를 함께 사용하는 경우가 많다.
  • PCA(n_components=0.8)처럼 설명 분산 비율을 기준으로 주성분 개수를 자동 결정할 수도 있다.
  • K-Means 군집 결과를 하나의 새로운 범주형 특징으로 활용하는 방법도 있다.
  • 특징을 만들 때는 모델 성능뿐 아니라 도메인 의미, 데이터 누수, 실제 예측 시점에서 사용할 수 있는 정보인지를 함께 확인해야 한다.

정리하며

특징 생성은 데이터를 많이 만들어내는 과정이 아니다.

원본 데이터에서 문제 해결에 필요한 정보를 꺼내고, 알고리즘이 이해하기 좋은 형태로 바꾸고, 필요하다면 너무 많은 정보를 압축하는 작업이다.

원본 데이터
    ↓
문제 정의
    ↓
의미 있는 특징 생성
    ↓
변환 결과 검증
    ↓
모델링

One-Hot Encoding이나 날짜 분해처럼 비교적 단순한 방법부터 PCA, 군집 분석처럼 알고리즘을 이용하는 방법까지 접근은 다양하다.

중요한 것은 어떤 변환을 사용했는지가 아니라, 왜 그 특징이 필요한지 설명할 수 있는가다.

profile
html_programming_language

0개의 댓글