데이터 분석이나 머신러닝을 시작하면 처음에는 원본 데이터를 깨끗하게 만드는 것에 집중하게 된다. 결측치를 처리하고, 이상치를 확인하고, 스케일을 맞추는 식이다.
그런데 전처리는 단순히 데이터를 정리하는 데서 끝나지 않는다. 원본 변수에서 분석에 더 유용한 정보를 뽑아내거나, 여러 변수를 새로운 형태로 표현하는 특징 생성(Feature Engineering) 도 중요한 과정이다.
예를 들어 서버 로그에 다음과 같은 값이 있다고 생각해보자.
접속 시각
디바이스 종류
요청 수
오류 수
평균 응답 시간
이 데이터를 그대로 사용할 수도 있지만, 실제 분석에서는 아래와 같은 정보가 더 유용할 수 있다.
접속 시각
↓
시간대 / 요일 / 주말 여부
요청 수 + 오류 수
↓
오류율
여러 성능 지표
↓
PCA로 압축된 주성분
여러 사용자 행동 지표
↓
K-Means 군집 번호
즉, 특징 생성은 단순히 컬럼 수를 늘리는 작업이 아니라 문제를 더 잘 설명할 수 있는 표현을 만드는 작업이라고 볼 수 있다.
좋은 모델을 사용해도 입력 데이터가 문제를 제대로 설명하지 못하면 좋은 결과를 얻기 어렵다.
원본 데이터에는 다음과 같은 한계가 있을 수 있다.
이런 문제를 해결하기 위해 특징을 새롭게 표현한다.
Raw Data
↓
의미 있는 정보 추출
↓
Feature Engineering
↓
분석 / 머신러닝 모델
특징 생성에서 중요한 점은 새로운 컬럼을 많이 만드는 것 자체가 목적이 아니라는 것이다. 분석 목적에 실제로 도움이 되는 정보인지를 계속 확인해야 한다.
머신러닝 알고리즘 중 많은 경우 입력값으로 수치형 데이터를 사용한다.
하지만 실제 서비스 데이터에는 문자열 범주가 자주 등장한다.
device
- desktop
- mobile
- tablet
이 값을 단순히 다음처럼 바꾸는 방법도 생각할 수 있다.
desktop → 1
mobile → 2
tablet → 3
하지만 이 방식은 실제로 존재하지 않는 순서 관계를 모델에 전달할 수 있다.
tablet = 3이 desktop = 1보다 세 배 큰 의미를 갖는 것은 아니기 때문이다.
순서가 없는 범주형 데이터에서는 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을 무조건 적용하기보다 다른 표현 방법을 검토해야 한다.
특징 생성에서 자주 사용하는 방법이 분해(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
분해와 반대로 기존 변수 여러 개를 결합(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라고 부르기도 한다.
예를 들어 API 서버에서는 CPU 사용률이 높다는 사실 하나보다 다음 상황이 더 중요할 수 있다.
CPU 사용률 높음
+
요청 수 많음
이를 하나의 특징으로 표현해보면 다음과 같다.
service["load_interaction"] = (
service["cpu_usage"]
* service["request_count"]
)
다만 아무 변수나 무작정 곱한다고 좋은 특징이 되는 것은 아니다.
도메인 지식과 문제 정의를 바탕으로 왜 이 관계가 필요한지 설명할 수 있어야 한다.
파생 변수를 만들다 보면 모델이 예측해야 하는 결과를 간접적으로 포함하는 실수가 생길 수 있다.
예를 들어 회원 탈퇴 여부를 예측하면서 탈퇴 이후에만 생성되는 값을 입력 변수로 사용하면 모델은 실제 예측이 아니라 정답을 미리 보는 것과 비슷해진다.
이것을 Target Leakage라고 한다.
특징을 만들 때는 다음 질문을 확인하는 습관이 중요하다.
이 값은 실제 예측 시점에도 알 수 있는 정보인가?
예측 시점에는 존재하지 않는 데이터라면 입력 특징으로 사용하면 안 된다.
특징 생성이라고 하면 새로운 컬럼을 추가하는 것만 떠올리기 쉽지만, 반대로 여러 변수의 정보를 압축해 더 적은 수의 새로운 특징을 만들 수도 있다.
대표적인 방법이 PCA(Principal Component Analysis, 주성분 분석) 다.
PCA는 서로 연관된 여러 수치형 변수의 정보를 새로운 축으로 재구성한다.
feature_1 ─┐
feature_2 ─┤
feature_3 ─┤
... ├─→ PCA → PC1, PC2, ...
feature_n ─┘
새로 생성된 PC1, PC2 같은 값이 주성분(Principal Component) 이다.
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개로 줄어든 것을 확인할 수 있었다.
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())
두 개의 주성분으로 축소했다면 산점도로 데이터를 표현하기 쉬워진다.
시각화를 위해 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()
여기서 시각화는 모델 성능을 증명하는 것이 아니라 축소된 특징 공간에서 데이터 구조를 탐색하는 용도로 보는 것이 좋다.
차원을 줄이는 또 다른 아이디어는 여러 변수로부터 군집(Cluster) 을 만들고 그 군집 번호를 새로운 특징으로 사용하는 것이다.
예를 들어 사용자 행동 데이터가 다음과 같다고 하자.
page_view
session_minutes
search_count
cart_count
purchase_count
이 여러 값을 기반으로 K-Means를 수행하면 사용자별로 하나의 군집 번호를 얻을 수 있다.
여러 행동 변수
↓
K-Means
↓
cluster = 0, 1, 2, ...
이 군집 번호를 이후 모델의 새로운 특징으로 사용할 수 있다.
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보다 크거나 우수하다는 뜻이 아니다.
따라서 모델에 따라서는 이 군집 번호를 다시 범주형 특징으로 처리하는 것도 검토할 수 있다.
두 방법 모두 여러 수치형 변수를 새로운 특징으로 압축할 수 있지만 의미는 다르다.
| 구분 | PCA | K-Means 기반 특징 |
|---|---|---|
| 핵심 목적 | 분산 정보를 보존하며 새로운 축 생성 | 비슷한 관측값을 그룹화 |
| 결과 | 연속형 주성분 | 군집 라벨 |
| 대표 결과 | pc1, pc2 | cluster |
| Scaling | 일반적으로 중요 | 거리 기반이므로 중요 |
| 활용 | 차원 축소, 시각화, 모델 입력 | 사용자 세그먼트, 추가 특징 |
| 해석 | 원본 변수보다 직관성이 낮을 수 있음 | 각 군집 특성을 별도로 분석 가능 |
PCA는 원래 변수의 정보를 여러 연속적인 축으로 압축한다.
반면 K-Means는 관측값을 비슷한 그룹으로 묶어 그룹 소속 자체를 새로운 특징으로 만든다.
헷갈리기 쉬운 개념이 특징 생성과 특징 선택이다.
기존 데이터를 변환해 새로운 특징을 만든다.
timestamp → hour, weekday
request / error → error_rate
여러 변수 → PCA
여러 행동 변수 → cluster
이미 존재하는 특징 중 필요한 것만 남긴다.
feature_1
feature_2
feature_3
feature_4
↓ 선택
feature_1
feature_3
두 방법 모두 모델 입력을 개선하기 위한 방법이지만 접근 방법은 다르다.
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은 다음 질문을 계속 던지는 과정에 가깝다.
현재 컬럼만으로 내가 풀려는 문제를 충분히 설명할 수 있는가?
코드가 에러 없이 실행됐다고 해서 특징 생성이 끝난 것은 아니다.
다음 항목을 함께 확인하는 것이 좋다.
범주형 변환
날짜 파생 변수
datetime 타입으로 변환됐는가?결합 특징
PCA
K-Means
k가 합리적인가?pd.get_dummies()를 이용하면 One-Hot Encoding을 쉽게 적용할 수 있다.pd.to_datetime()으로 변환한 뒤 연도, 월, 요일, 시간 등의 특징으로 분해할 수 있다.PCA(n_components=0.8)처럼 설명 분산 비율을 기준으로 주성분 개수를 자동 결정할 수도 있다.특징 생성은 데이터를 많이 만들어내는 과정이 아니다.
원본 데이터에서 문제 해결에 필요한 정보를 꺼내고, 알고리즘이 이해하기 좋은 형태로 바꾸고, 필요하다면 너무 많은 정보를 압축하는 작업이다.
원본 데이터
↓
문제 정의
↓
의미 있는 특징 생성
↓
변환 결과 검증
↓
모델링
One-Hot Encoding이나 날짜 분해처럼 비교적 단순한 방법부터 PCA, 군집 분석처럼 알고리즘을 이용하는 방법까지 접근은 다양하다.
중요한 것은 어떤 변환을 사용했는지가 아니라, 왜 그 특징이 필요한지 설명할 수 있는가다.