특성 공학 완전 정복: 특성 선택·추출·정규화·PCA와 실전 피처 생성
특성 공학(Feature Engineering)은 원시 데이터를 머신러닝 모델이 학습하기 좋은 표현으로 바꾸는 과정입니다.
예를 들어 전자상거래 고객 이탈을 예측한다고 가정해 보겠습니다. 원본 데이터에는 주문 시각, 결제 금액, 상품 ID, 접속 로그처럼 개별 사건만 들어 있을 수 있습니다. 그러나 모델이 실제로 필요로 하는 정보는 다음과 같은 형태일 가능성이 큽니다.
- 최근 7일 접속 횟수
- 마지막 구매 후 경과 일수
- 최근 30일 평균 주문 금액
- 이전 30일 대비 구매 빈도 변화율
- 고객이 주로 활동하는 시간대
- 환불 비율과 문의 빈도
- 같은 고객의 장기 평균에서 벗어난 정도
원시 데이터에 없던 이런 변수를 설계하는 일이 특성 공학입니다.
좋은 특성은 모델이 복잡한 규칙을 처음부터 모두 찾아내도록 맡기지 않고, 문제의 구조와 예측 시점에 존재하는 정보를 학습 가능한 신호로 압축합니다. 반대로 잘못 만든 특성은 데이터 누수, 과적합, 운영 불일치, 편향 확대를 일으킬 수 있습니다.
이 글에서는 정형 데이터뿐 아니라 시계열, 텍스트, 이미지, 센서 데이터까지 포함해 특성 공학을 체계적으로 정리합니다.
핵심 요약
특성 공학에서 가장 중요한 원칙은 다음과 같습니다.
- 예측 시점을 먼저 정의한다. 특성은 실제 예측 시점에 알 수 있는 정보만 사용해야 합니다.
- 분할 전에 전체 데이터로 통계를 계산하지 않는다. 평균, 표준편차, 범주 빈도, 타깃 평균, 특성 선택 기준은 훈련 데이터에서만 학습해야 합니다.
- 도메인 의미를 보존한다. 단순 변환보다 비율, 차이, 경과 시간, 변화량처럼 문제 구조를 반영한 특성이 강력한 경우가 많습니다.
- 복잡한 특성이 항상 좋은 것은 아니다. 새로운 특성은 교차검증과 시간 외 검증으로 실제 기여도를 확인해야 합니다.
- 학습과 서빙에서 동일한 계산을 사용한다. 오프라인 학습 코드와 온라인 추론 코드가 다르면 성능이 쉽게 무너집니다.
- 특성 생성과 특성 선택을 구분한다. 많은 후보를 만드는 일과 실제 사용할 특성을 고르는 일은 서로 다른 단계입니다.
- 특성의 정의·출처·시점·단위·버전을 기록한다. 모델 재현성과 운영 안정성을 위해 특성도 데이터 자산으로 관리해야 합니다.
1. 특성이란 무엇인가
머신러닝에서 특성(feature)은 모델에 입력되는 개별 변수입니다.
데이터 행 하나를 \(i\)번째 샘플이라고 하면 입력은 보통 다음처럼 표현합니다.
$$ \mathbf{x}_i = \left[x_{i1}, x_{i2}, \dots, x_{ip}\right] $$
여기서 \(p\)는 특성 수입니다.
예를 들어 주택 가격 예측 모델의 입력은 다음과 같을 수 있습니다.
면적, 방 개수, 건축 연도, 역과의 거리, 지역, 최근 거래량
하지만 데이터베이스에 저장된 원본 열이 곧바로 좋은 특성이 되는 것은 아닙니다.
계약일시 = 2026-07-15 14:32:18
이 값에서 다음과 같은 여러 특성을 만들 수 있습니다.
계약 연도
계약 월
요일
주말 여부
시간대
분기
연말 여부
해당 지역의 최근 30일 거래량
직전 거래 후 경과 시간
즉, 하나의 원시 열이 여러 특성으로 확장될 수 있고, 여러 원시 열을 결합해 하나의 특성을 만들 수도 있습니다.
2. 특성 공학과 데이터 전처리의 차이
두 용어는 자주 섞여 사용되지만 초점이 다릅니다.
| 구분 | 목적 | 대표 작업 |
|---|---|---|
| 데이터 정제 | 오류와 불일치 제거 | 중복 제거, 타입 수정, 비정상 값 처리 |
| 데이터 전처리 | 모델 입력 형식으로 변환 | 결측치 대체, 스케일링, 인코딩 |
| 특성 공학 | 예측에 유용한 새로운 표현 생성 | 비율, 집계, 경과 시간, 상호작용 |
| 특성 선택 | 불필요한 특성 제거 | 필터, 래퍼, 임베디드 방법 |
| 차원 축소 | 정보를 더 작은 공간으로 압축 | PCA, SVD, 오토인코더 |
예를 들어 매출 데이터에서 누락된 가격을 중앙값으로 대체하는 것은 전처리에 가깝습니다. 반면 다음 변수는 특성 공학에 해당합니다.
할인율 = (정가 - 판매가) / 정가
고객별 최근 30일 구매액
직전 구매 대비 주문 금액 변화율
실제 프로젝트에서는 이 단계들이 하나의 파이프라인 안에서 함께 수행됩니다. 중요한 것은 용어 구분 자체보다 각 변환이 어떤 데이터를 보고 학습되는지와 예측 시점에 계산 가능한지입니다.
3. 좋은 특성의 조건
3-1. 예측 대상과 관련이 있어야 한다
좋은 특성은 타깃과 통계적으로 또는 인과적으로 관련된 신호를 포함합니다.
고객 이탈 예측에서 고객 ID 자체는 대부분 의미가 없지만 다음 특성은 유용할 수 있습니다.
- 마지막 접속 후 경과 일수
- 최근 접속 빈도의 감소율
- 최근 결제 실패 횟수
- 고객센터 문의 후 해결까지 걸린 시간
다만 타깃과 상관관계가 높다고 해서 항상 좋은 특성은 아닙니다. 결과가 발생한 뒤 생성된 값이라면 데이터 누수일 수 있습니다.
3-2. 실제 추론 시점에 계산 가능해야 한다
부도 예측 모델에 채권 추심 완료 여부를 넣으면 높은 성능이 나올 수 있지만, 부도 전에 예측하려는 목적에는 사용할 수 없습니다.
각 특성에는 다음 질문을 적용해야 합니다.
이 값은 모델이 예측을 수행하는 정확한 시점에 이미 존재하는가?
3-3. 훈련과 운영에서 같은 의미여야 한다
훈련 데이터의 최근 30일 구매액이 달력 기준 30일인데 운영 코드가 최근 720시간을 사용한다면 경계 조건에서 값이 달라집니다.
시간대, 결측 처리, 반올림, 제외 조건, 환율, 상태 코드 해석도 동일해야 합니다.
3-4. 미래 데이터 변화에 견뎌야 한다
훈련 기간에만 우연히 존재한 규칙을 특성으로 사용하면 운영 성능이 떨어집니다.
예:
- 특정 캠페인 코드
- 일시적인 상품 ID
- 테스트 기간에만 존재한 서버 이름
- 담당자 수기 입력 습관
- 촬영 장비의 워터마크
- 클래스와 우연히 연결된 파일명
좋은 특성은 가능한 한 문제의 본질을 반영해야 합니다.
3-5. 설명하고 검증할 수 있어야 한다
모든 특성이 사람이 직관적으로 이해 가능해야 하는 것은 아닙니다. 임베딩처럼 고차원 특성도 유용합니다.
그러나 최소한 다음은 기록해야 합니다.
- 특성 이름
- 계산식
- 원본 데이터
- 기준 시점
- 집계 구간
- 단위
- 결측 처리
- 갱신 주기
- 허용 범위
- 버전
4. 특성 공학의 전체 절차
특성 공학은 보통 다음 순서로 진행합니다.
문제 정의
→ 예측 시점 정의
→ 원시 데이터와 엔터티 확인
→ 데이터 분할 전략 결정
→ 기본 전처리 파이프라인 구축
→ 도메인 기반 특성 후보 생성
→ 교차검증과 제거 실험
→ 특성 선택
→ 운영 계산 방식 고정
→ 모니터링
여기서 가장 중요한 순서는 분할 전략을 특성 생성보다 먼저 정하는 것입니다.
전체 데이터에서 고객 평균, 상품 인기도, 타깃 평균을 계산한 뒤 훈련·검증 세트로 나누면 검증 데이터의 정보가 이미 특성에 섞입니다.
5. 데이터 누수: 특성 공학의 가장 위험한 실패
5-1. 타깃 누수
타깃 또는 타깃의 직접적인 결과가 입력에 포함되는 경우입니다.
예:
- 이탈 예측에 해지 완료 코드
- 부도 예측에 연체 확정 상태
- 질병 예측에 확진 후 처방된 약
- 불량 예측에 최종 검사 판정
- 감정 분류에 별점
- 합격 예측에 최종 합격자 ID 목록
5-2. 시간 누수
예측 기준일 이후의 데이터가 집계에 포함되는 경우입니다.
예측 시점: 7월 1일 00:00
잘못된 특성: 6월 15일~7월 15일 평균 사용량
올바른 특성: 6월 1일~6월 30일 평균 사용량
시계열에서는 단순히 미래 행을 사용하지 않는 것만으로 충분하지 않습니다. 롤링 윈도우의 끝, 데이터 확정 지연, 수정 이력까지 확인해야 합니다.
5-3. 전처리 누수
전체 데이터로 다음 값을 계산하면 검증·테스트 정보가 훈련에 들어갑니다.
- 평균과 표준편차
- 최솟값과 최댓값
- 결측치 대체값
- 범주 빈도
- PCA 축
- 특성 선택 점수
- 이상치 기준
- 타깃 인코딩 값
안전한 구조는 전처리와 특성 선택을 모델 파이프라인 내부에 넣는 것입니다. scikit-learn의 Pipeline과 ColumnTransformer는 서로 다른 열에 변환을 적용하고, 교차검증의 각 훈련 폴드에서 변환기를 다시 학습하도록 구성하는 데 적합합니다.
출처: scikit-learn — Pipelines and composite estimators, ColumnTransformer
5-4. 그룹 누수
같은 고객, 환자, 장비, 문서, 촬영 장면이 훈련과 검증에 동시에 들어가면 모델이 일반 규칙 대신 개체 고유 정보를 기억할 수 있습니다.
group_columns = [
"customer_id",
"patient_id",
"device_id",
"source_document_id",
]
for column in group_columns:
if column not in train_df.columns:
continue
overlap = set(train_df[column]) & set(valid_df[column])
print(column, "겹치는 그룹 수:", len(overlap))
5-5. 집계 특성의 누수
고객별 전체 기간 평균 구매액을 먼저 계산한 뒤 과거 시점 예측에 사용하면 미래 구매가 포함됩니다.
안전한 집계는 기준 행보다 과거 데이터만 사용해야 합니다.
df = df.sort_values(["customer_id", "event_time"])
df["past_order_count"] = (
df.groupby("customer_id")
.cumcount()
)
df["past_amount_mean"] = (
df.groupby("customer_id")["amount"]
.transform(lambda s: s.shift(1).expanding().mean())
)
shift(1)은 현재 행의 값이 자신의 과거 집계에 포함되는 것을 막습니다.
6. 수치형 특성 공학
6-1. 스케일링
수치형 변수의 범위를 맞추는 대표 방법은 다음과 같습니다.
표준화
$$ z = \frac{x – \mu}{\sigma} $$
평균 0, 표준편차 1에 가깝게 변환합니다.
로지스틱 회귀, 선형 SVM, 신경망, 거리 기반 모델에서 유용합니다.
최소-최대 정규화
$$ x’ = \frac{x – x_{\min}}{x_{\max} – x_{\min}} $$
일반적으로 0과 1 사이로 변환합니다.
강건 스케일링
중앙값과 사분위 범위를 사용합니다.
$$ x’ = \frac{x – \operatorname{median}(x)}{Q_3 – Q_1} $$
극단값의 영향을 줄이고 싶을 때 사용할 수 있습니다.
트리 기반 모델은 일반적으로 단조 변환과 스케일에 덜 민감하지만, 거리 계산이나 규제, 여러 모델의 결합을 고려하면 스케일링이 필요할 수 있습니다.
6-2. 로그 변환
매출, 소득, 파일 크기, 조회수처럼 오른쪽 꼬리가 긴 분포에는 로그 변환이 유용할 수 있습니다.
$$ x’ = \log(1 + x) $$
import numpy as np
df["log_revenue"] = np.log1p(df["revenue"].clip(lower=0))
log1p는 \(x=0\)에서도 계산할 수 있습니다.
음수가 존재하면 단순 로그를 사용할 수 없으므로 데이터 의미에 맞는 이동, 부호 보존 로그, Yeo-Johnson 변환 등을 검토해야 합니다.
6-3. 구간화
연속값을 범주 구간으로 바꾸는 방법입니다.
import pandas as pd
df["age_group"] = pd.cut(
df["age"],
bins=[0, 19, 29, 39, 49, 59, float("inf")],
labels=["10대 이하", "20대", "30대", "40대", "50대", "60대 이상"],
right=True,
)
구간화는 비선형 관계를 단순 모델이 학습하기 쉽게 만들 수 있지만 정보가 손실됩니다. 경계값 근처의 두 값이 크게 다른 범주로 나뉘는 문제도 있습니다.
6-4. 비율과 차이
도메인에서 의미 있는 비율은 원시값보다 강한 신호가 될 수 있습니다.
eps = 1e-8
df["discount_rate"] = (
(df["list_price"] - df["sale_price"])
/ (df["list_price"].abs() + eps)
)
df["debt_to_income"] = (
df["monthly_debt"]
/ (df["monthly_income"].abs() + eps)
)
df["temperature_gap"] = (
df["indoor_temperature"] - df["outdoor_temperature"]
)
분모가 0에 가까울 때 값이 폭발할 수 있으므로 예외 처리가 필요합니다.
6-5. 상호작용 특성
두 변수의 결합 효과를 표현합니다.
df["area_per_room"] = (
df["area_m2"] / df["room_count"].clip(lower=1)
)
df["price_x_discount"] = (
df["price"] * df["discount_rate"]
)
선형 모델은 상호작용 항이 없으면 변수 조합 효과를 직접 표현하기 어렵습니다.
다항식 특성을 자동 생성할 수도 있습니다.
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(
degree=2,
include_bias=False,
interaction_only=True,
)
특성 수가 \(p\)일 때 고차 다항식은 차원을 빠르게 증가시키므로 규제와 특성 선택이 필요합니다.
6-6. 기준값에서 벗어난 정도
절대값보다 개인 또는 장비의 평소 상태에서 얼마나 벗어났는지가 중요할 수 있습니다.
group_mean = df.groupby("device_id")["temperature"].transform("mean")
group_std = df.groupby("device_id")["temperature"].transform("std")
df["temperature_group_z"] = (
(df["temperature"] - group_mean)
/ group_std.replace(0, float("nan"))
)
단, 전체 기간 통계를 사용하면 시간 누수가 생길 수 있습니다. 운영 예측에서는 과거 데이터만 사용한 이동 기준값을 계산해야 합니다.
7. 결측치도 하나의 신호일 수 있다
결측값은 단순한 데이터 손상이 아닐 수 있습니다.
예:
- 검사를 받지 않아 검사값이 없음
- 특정 요금제에서만 항목이 기록되지 않음
- 센서 고장으로 값이 끊김
- 고객이 선택 입력을 건너뜀
- 신규 사용자라 과거 이력이 없음
따라서 값 대체와 함께 결측 여부 특성을 추가할 수 있습니다.
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(
strategy="median",
add_indicator=True,
)
다만 결측 패턴이 운영 환경에서 바뀌면 모델이 취약해질 수 있습니다. 결측률 자체도 모니터링해야 합니다.
8. 범주형 특성 공학
8-1. 원-핫 인코딩
순서가 없는 저카디널리티 범주에 적합합니다.
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(
handle_unknown="ignore",
min_frequency=10,
)
handle_unknown="ignore"는 학습에 없던 신규 범주가 운영에서 등장했을 때 오류를 막습니다.
8-2. 순서형 인코딩
등급처럼 실제 순서가 있는 범주는 명시적으로 순서를 지정할 수 있습니다.
from sklearn.preprocessing import OrdinalEncoder
encoder = OrdinalEncoder(
categories=[["bronze", "silver", "gold", "platinum"]],
handle_unknown="use_encoded_value",
unknown_value=-1,
)
지역 코드나 상품 ID처럼 순서가 없는 값을 임의의 정수로 바꾸면 모델이 숫자 크기에 의미가 있다고 오해할 수 있습니다.
8-3. 빈도 인코딩
각 범주가 등장한 빈도를 사용합니다.
frequency = (
train_df["category"]
.value_counts(normalize=True)
)
train_df["category_frequency"] = (
train_df["category"]
.map(frequency)
.fillna(0)
)
valid_df["category_frequency"] = (
valid_df["category"]
.map(frequency)
.fillna(0)
)
빈도표는 반드시 훈련 데이터에서 계산해야 합니다.
8-4. 타깃 인코딩
범주별 타깃 평균을 특성으로 사용합니다.
$$ \operatorname{TE}(c) = \frac{ n_c \bar{y}_c + \alpha \bar{y} }{ n_c + \alpha } $$
여기서 \(n_c\)는 범주 \(c\)의 샘플 수, \(\bar{y}_c\)는 범주별 평균, \(\bar{y}\)는 전체 평균, \(\alpha\)는 평활화 강도입니다.
타깃 인코딩은 강력하지만 누수와 과적합 위험이 큽니다. 훈련 행 자신의 타깃이 인코딩 값에 들어가지 않도록 교차적합(cross-fitting) 방식이 필요합니다.
from sklearn.preprocessing import TargetEncoder
encoder = TargetEncoder(
smooth="auto",
cv=5,
random_state=42,
)
scikit-learn의 TargetEncoder.fit_transform은 내부 교차적합을 사용하도록 설계되어 있으며, 단순히 전체 훈련 데이터에서 범주별 평균을 계산해 같은 행에 다시 넣는 방식과 다릅니다.
출처: scikit-learn — TargetEncoder
8-5. 희귀 범주 묶기
빈도가 매우 낮은 범주는 과적합과 차원 증가를 일으킬 수 있습니다.
counts = train_df["category"].value_counts()
rare_categories = counts[counts < 20].index
train_df["category_reduced"] = (
train_df["category"]
.where(~train_df["category"].isin(rare_categories), "OTHER")
)
검증과 운영에서도 훈련 데이터에서 정한 희귀 범주 목록을 사용해야 합니다.
8-6. 범주 조합
두 범주의 조합이 중요할 수 있습니다.
df["region_device"] = (
df["region"].astype(str)
+ "__"
+ df["device_type"].astype(str)
)
조합 수가 지나치게 커지면 희소성과 과적합이 커지므로 최소 빈도 제한이나 해싱을 함께 고려합니다.
9. 날짜와 시간 특성
날짜를 문자열이나 Unix timestamp 하나로 넣는 것보다 구조를 분해하는 편이 유용한 경우가 많습니다.
dt = pd.to_datetime(df["event_time"], utc=True)
df["year"] = dt.dt.year
df["month"] = dt.dt.month
df["day"] = dt.dt.day
df["dayofweek"] = dt.dt.dayofweek
df["hour"] = dt.dt.hour
df["is_weekend"] = dt.dt.dayofweek >= 5
df["quarter"] = dt.dt.quarter
df["dayofyear"] = dt.dt.dayofyear
9-1. 경과 시간
df["days_since_signup"] = (
dt - pd.to_datetime(df["signup_time"], utc=True)
).dt.total_seconds() / 86400
많은 문제에서 절대 날짜보다 특정 사건 후 경과 시간이 더 의미 있습니다.
9-2. 주기형 인코딩
시간, 요일, 월은 끝과 시작이 연결된 순환 구조입니다. 23시와 0시는 숫자로는 멀지만 실제로는 가깝습니다.
$$ x_{\sin} = \sin\left(2\pi \frac{x}{P}\right) $$
$$ x_{\cos} = \cos\left(2\pi \frac{x}{P}\right) $$
import numpy as np
df["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24)
df["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24)
df["dow_sin"] = np.sin(2 * np.pi * df["dayofweek"] / 7)
df["dow_cos"] = np.cos(2 * np.pi * df["dayofweek"] / 7)
9-3. 달력과 업무 맥락
다음 특성도 문제에 따라 유용합니다.
- 공휴일 여부
- 공휴일 전날·다음 날
- 월초·월말
- 급여일과의 거리
- 학기·방학
- 계절
- 영업 시간 여부
- 프로모션 기간
- 기상 관측 시각과의 정렬 여부
공휴일과 시간대는 국가·지역·연도에 따라 달라지므로 운영에서 정확한 캘린더를 사용해야 합니다.
10. 시계열 특성 공학
시계열 특성은 반드시 예측 기준 시점보다 과거 데이터만 사용해야 합니다.
10-1. 지연 특성
df = df.sort_values(["device_id", "timestamp"])
for lag in [1, 2, 3, 6, 12, 24]:
df[f"value_lag_{lag}"] = (
df.groupby("device_id")["value"]
.shift(lag)
)
10-2. 변화량과 변화율
df["diff_1"] = (
df.groupby("device_id")["value"]
.diff(1)
)
df["pct_change_1"] = (
df.groupby("device_id")["value"]
.pct_change(1)
.replace([float("inf"), float("-inf")], float("nan"))
)
10-3. 이동 통계
현재 시점 값을 제외하기 위해 먼저 shift(1)을 적용합니다.
grouped = df.groupby("device_id")["value"]
past_value = grouped.shift(1)
df["rolling_mean_6"] = (
past_value
.groupby(df["device_id"])
.rolling(6, min_periods=3)
.mean()
.reset_index(level=0, drop=True)
)
df["rolling_std_24"] = (
past_value
.groupby(df["device_id"])
.rolling(24, min_periods=6)
.std()
.reset_index(level=0, drop=True)
)
이동 통계 후보:
- 평균
- 중앙값
- 표준편차
- 최솟값·최댓값
- 분위수
- 합계
- 0이 아닌 횟수
- 임계값 초과 횟수
- 기울기
- 최근 값과 이동평균의 차이
10-4. 지수 가중 통계
최근 관측값에 더 큰 가중치를 줍니다.
df["ewm_mean_12"] = (
df.groupby("device_id")["value"]
.transform(lambda s: s.shift(1).ewm(span=12).mean())
)
10-5. 같은 계절 위치와 비교
전력 사용량처럼 일·주 단위 주기가 있는 경우 다음 특성이 유용할 수 있습니다.
24시간 전 값
7일 전 같은 시각 값
최근 4주 같은 요일·시간 평균
현재 값과 7일 전 값의 차이
10-6. 불규칙 간격
이벤트 간격이 일정하지 않다면 단순 행 기준 lag보다 시간 기준 특성이 필요합니다.
df["seconds_since_previous"] = (
df.groupby("device_id")["timestamp"]
.diff()
.dt.total_seconds()
)
센서가 누락된 구간에는 관측 개수, 마지막 관측 후 경과 시간, 결측 연속 길이도 특성이 될 수 있습니다.
11. 집계 특성
개별 이벤트를 고객·상품·장비 단위의 요약으로 바꾸는 방식입니다.
customer_features = (
orders.groupby("customer_id")
.agg(
order_count=("order_id", "nunique"),
total_amount=("amount", "sum"),
average_amount=("amount", "mean"),
max_amount=("amount", "max"),
refund_rate=("is_refund", "mean"),
active_days=("order_date", "nunique"),
)
.reset_index()
)
11-1. 최근성·빈도·금액
고객 행동에서는 RFM 특성이 널리 사용됩니다.
- Recency: 마지막 구매 후 경과 시간
- Frequency: 일정 기간 구매 횟수
- Monetary: 일정 기간 구매 금액
reference_time = pd.Timestamp("2026-07-01", tz="UTC")
rfm = (
orders[orders["order_time"] < reference_time]
.groupby("customer_id")
.agg(
last_order_time=("order_time", "max"),
frequency=("order_id", "nunique"),
monetary=("amount", "sum"),
)
)
rfm["recency_days"] = (
reference_time - rfm["last_order_time"]
).dt.total_seconds() / 86400
11-2. 여러 기간의 집계
하나의 기간만 사용하면 최근 변화와 장기 기준을 동시에 보기 어렵습니다.
최근 1일 합계
최근 7일 합계
최근 30일 합계
최근 7일 평균 / 최근 30일 평균
최근 7일 빈도 - 이전 7일 빈도
11-3. 변화 추세
df["usage_ratio_7d_30d"] = (
df["usage_sum_7d"]
/ df["usage_sum_30d"].replace(0, float("nan"))
)
df["orders_change"] = (
df["orders_last_7d"] - df["orders_prev_7d"]
)
절대 수준과 변화 방향을 함께 제공하면 모델이 상태와 추세를 구분할 수 있습니다.
12. 텍스트 특성 공학
12-1. 기본 통계 특성
text = df["text"].fillna("")
df["char_count"] = text.str.len()
df["word_count"] = text.str.split().str.len()
df["line_count"] = text.str.count(r"\n") + 1
df["digit_count"] = text.str.count(r"\d")
df["question_count"] = text.str.count(r"\?")
df["exclamation_count"] = text.str.count(r"!")
df["url_count"] = text.str.count(r"https?://")
문자 수, 숫자 비율, URL 수 같은 단순 특성도 스팸 탐지, 문서 분류, 품질 판정에서 유용할 수 있습니다.
12-2. Bag-of-Words와 TF-IDF
TF-IDF는 문서 내에서 자주 등장하지만 전체 문서에서는 드문 단어에 더 큰 가중치를 줍니다.
$$ \operatorname{tfidf}(t,d) = \operatorname{tf}(t,d) \cdot \log\frac{N}{\operatorname{df}(t)} $$
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(
ngram_range=(1, 2),
min_df=3,
max_df=0.95,
max_features=50000,
sublinear_tf=True,
)
한국어에서는 형태소 분석, 문자 n-gram, 서브워드 방식 중 문제와 데이터 규모에 맞는 방법을 비교해야 합니다.
12-3. 임베딩
문장·문서 임베딩은 의미적으로 유사한 텍스트를 가까운 벡터로 표현합니다.
활용 예:
- 문서 분류
- 검색
- 중복 탐지
- 군집화
- 추천
- RAG 검색 특성
임베딩을 사용할 때는 모델 버전, 차원, 정규화 여부, 입력 잘림 길이를 기록해야 합니다.
12-4. 메타데이터 결합
텍스트 표현만 사용하지 않고 다음 정보를 함께 넣을 수 있습니다.
- 작성 시각
- 작성자 유형
- 언어
- 문서 길이
- 출처
- 카테고리
- 이전 문서와의 유사도
- 링크 수
- 첨부파일 존재 여부
다만 작성자 ID나 출처가 타깃을 직접 대리하는지 확인해야 합니다.
13. 이미지 특성 공학
딥러닝에서는 CNN이나 Vision Transformer가 이미지 표현을 자동으로 학습하지만, 데이터와 목적에 따라 명시적 특성이 여전히 유용합니다.
13-1. 기본 이미지 메타 특성
from PIL import Image
from pathlib import Path
def image_metadata(path: str) -> dict:
with Image.open(path) as image:
width, height = image.size
return {
"width": width,
"height": height,
"aspect_ratio": width / max(height, 1),
"megapixels": width * height / 1_000_000,
"mode": image.mode,
"format": image.format,
}
13-2. 픽셀 통계
- 채널별 평균과 표준편차
- 밝기
- 대비
- 채도
- 흐림 정도
- 어두운 픽셀 비율
- 과노출 픽셀 비율
- 경계 밀도
- 색상 히스토그램
품질 검사, 촬영 환경 탐지, 이상 이미지 필터링에 사용할 수 있습니다.
13-3. 객체 기반 특성
객체 탐지 결과에서 다음 특성을 만들 수 있습니다.
- 객체 수
- 클래스별 객체 수
- 가장 큰 객체 면적 비율
- 객체 중심 위치
- 객체 간 거리
- 박스 중첩률
- 화면 가장자리와의 거리
df["box_width"] = df["x2"] - df["x1"]
df["box_height"] = df["y2"] - df["y1"]
df["box_area_ratio"] = (
df["box_width"] * df["box_height"]
/ (df["image_width"] * df["image_height"])
)
13-4. 사전학습 모델의 임베딩
사전학습 비전 모델의 중간 표현을 추출해 전통적인 분류기나 검색 시스템의 입력으로 사용할 수 있습니다.
이 경우 이미지 전처리, 크롭 방식, 모델 버전, 레이어, 정규화 방식을 고정해야 합니다.
13-5. 배경 편향 주의
모델이 객체 대신 배경, 워터마크, 테두리, 촬영 장비, 파일 해상도를 학습할 수 있습니다.
특성 중요도가 높더라도 실제 문제와 무관한 지름길(shortcut)일 수 있으므로 배경 교란 실험, 장비별 평가, 위치별 평가가 필요합니다.
14. 센서와 산업 데이터 특성 공학
센서 데이터에서는 절대값뿐 아니라 변화 패턴과 센서 간 관계가 중요합니다.
14-1. 통계 특성
일정 구간에서 다음을 계산할 수 있습니다.
- 평균
- 표준편차
- RMS
- 최솟값·최댓값
- 첨도
- 왜도
- 사분위 범위
- 피크 수
- 임계값 초과 시간
- 연속 초과 길이
RMS는 다음과 같습니다.
$$ x_{\mathrm{RMS}} = \sqrt{ \frac{1}{n} \sum_{i=1}^{n} x_i^2 } $$
14-2. 변화와 기울기
import numpy as np
def linear_slope(values):
values = np.asarray(values, dtype=float)
if len(values) < 2:
return np.nan
x = np.arange(len(values))
return np.polyfit(x, values, deg=1)[0]
온도 상승 속도, 진동 증가율, 압력 회복 속도는 고장 징후를 잘 나타낼 수 있습니다.
14-3. 주파수 영역
진동과 음향 데이터는 FFT 기반 특성을 사용할 수 있습니다.
- 지배 주파수
- 주파수 대역별 에너지
- 스펙트럼 중심
- 스펙트럼 엔트로피
- 고조파 비율
14-4. 센서 간 차이와 비율
df["temp_gap_1_2"] = df["temp_1"] - df["temp_2"]
df["current_imbalance"] = (
df[["current_1", "current_2", "current_3"]].max(axis=1)
- df[["current_1", "current_2", "current_3"]].min(axis=1)
)
센서 간 불균형은 개별 센서 절대값보다 이상 상태를 잘 드러낼 수 있습니다.
15. 특성 선택
많은 특성을 만드는 것과 모두 사용하는 것은 다릅니다.
불필요한 특성은 다음 문제를 일으킬 수 있습니다.
- 과적합
- 학습 시간 증가
- 추론 지연
- 메모리 증가
- 설명 가능성 저하
- 데이터 수집 비용 증가
- 운영 장애 가능성 증가
특성 선택 방법은 크게 필터, 래퍼, 임베디드 방식으로 나뉩니다.
15-1. 필터 방식
모델 학습과 독립적으로 통계량을 사용합니다.
- 분산 임계값
- 상관계수
- 카이제곱 검정
- ANOVA F-test
- 상호정보량
from sklearn.feature_selection import SelectKBest, mutual_info_classif
selector = SelectKBest(
score_func=mutual_info_classif,
k=20,
)
장점은 빠르다는 것이고, 단점은 특성 간 조합 효과를 충분히 반영하지 못할 수 있다는 것입니다.
15-2. 래퍼 방식
특성 부분집합을 바꿔 가며 모델 성능을 평가합니다.
- 전진 선택
- 후진 제거
- 순차 특성 선택
- 재귀적 특성 제거
scikit-learn의 SequentialFeatureSelector는 교차검증 점수를 기준으로 특성을 순차적으로 추가하거나 제거합니다.
출처: scikit-learn — SequentialFeatureSelector
래퍼 방식은 계산 비용이 큽니다.
15-3. 임베디드 방식
모델 학습 과정에서 특성 선택이 일어납니다.
- L1 규제
- Elastic Net
- 트리 기반 중요도
SelectFromModel
from sklearn.feature_selection import SelectFromModel
from sklearn.linear_model import LogisticRegression
selector = SelectFromModel(
LogisticRegression(
penalty="l1",
solver="liblinear",
C=0.1,
)
)
15-4. 특성 중요도 해석의 함정
트리의 불순도 기반 중요도는 연속값이나 가능한 분할점이 많은 특성에 편향될 수 있습니다. 상관된 두 특성은 중요도가 나뉘거나 한쪽만 선택될 수 있습니다.
따라서 다음을 함께 검토합니다.
- 순열 중요도
- 제거 실험
- SHAP 값
- 시간 외 검증 성능
- 그룹별 성능
- 운영 비용
특성 중요도가 낮다고 해서 반드시 제거해야 하는 것은 아닙니다. 다른 특성과의 상호작용에서만 유용할 수 있습니다.
16. 특성 생성 실험을 평가하는 방법
새로운 특성의 효과는 같은 데이터 분할과 같은 평가 조건에서 비교해야 합니다.
기준 모델
→ 특성 A 추가
→ 특성 B 추가
→ A+B 추가
→ 불필요한 특성 제거
16-1. 제거 실험
특성 그룹별로 제거해 성능 변화를 봅니다.
| 실험 | 사용 특성 | 검증 점수 |
|---|---|---|
| 기준 | 원시 특성 | 0.812 |
| 실험 A | 기준 + 최근성 | 0.834 |
| 실험 B | 기준 + 최근성 + 변화율 | 0.842 |
| 실험 C | 실험 B – 고객 ID | 0.844 |
점수 차이가 작다면 여러 시드나 교차검증 폴드에서 일관적인지 확인해야 합니다.
16-2. 시간 외 검증
시계열 또는 변화가 빠른 서비스에서는 무작위 분할보다 과거로 학습하고 미래로 검증하는 방식이 현실적입니다.
훈련: 1월~4월
검증: 5월
테스트: 6월
16-3. 그룹별 평가
전체 평균만 보면 특정 조건에서 실패하는 특성을 놓칠 수 있습니다.
- 신규 사용자와 기존 사용자
- 장비 유형
- 지역
- 시간대
- 클래스
- 데이터 품질 수준
- 결측률 구간
- 희귀 범주
16-4. 성능 외 비용
새로운 특성이 0.1% 성능을 높이지만 실시간 계산에 외부 API 호출이 필요하다면 운영 가치가 낮을 수 있습니다.
함께 평가할 항목:
- 계산 지연
- 저장 공간
- 데이터 수집 비용
- 장애 가능성
- 갱신 주기
- 개인정보 위험
- 설명 가능성
- 재현성
17. scikit-learn 파이프라인 실전 예제
다음 예제는 수치형, 범주형, 날짜 파생 특성을 하나의 파이프라인으로 구성합니다.
from __future__ import annotations
import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
class DateFeatureExtractor(BaseEstimator, TransformerMixin):
def __init__(self, column: str):
self.column = column
def fit(self, X, y=None):
return self
def transform(self, X):
frame = X.copy()
dt = pd.to_datetime(frame[self.column], utc=True, errors="coerce")
result = pd.DataFrame(
{
"month": dt.dt.month,
"dayofweek": dt.dt.dayofweek,
"hour": dt.dt.hour,
"is_weekend": (dt.dt.dayofweek >= 5).astype(float),
"hour_sin": np.sin(2 * np.pi * dt.dt.hour / 24),
"hour_cos": np.cos(2 * np.pi * dt.dt.hour / 24),
},
index=frame.index,
)
return result
numeric_features = [
"age",
"income",
"usage_30d",
"days_since_last_login",
]
categorical_features = [
"region",
"device_type",
"plan_type",
]
date_feature = ["event_time"]
numeric_pipeline = Pipeline(
steps=[
(
"imputer",
SimpleImputer(
strategy="median",
add_indicator=True,
),
),
("scaler", StandardScaler()),
]
)
categorical_pipeline = Pipeline(
steps=[
(
"imputer",
SimpleImputer(strategy="most_frequent"),
),
(
"onehot",
OneHotEncoder(
handle_unknown="ignore",
min_frequency=10,
),
),
]
)
date_pipeline = Pipeline(
steps=[
(
"date_features",
DateFeatureExtractor(column="event_time"),
),
(
"imputer",
SimpleImputer(strategy="most_frequent"),
),
("scaler", StandardScaler()),
]
)
preprocessor = ColumnTransformer(
transformers=[
(
"numeric",
numeric_pipeline,
numeric_features,
),
(
"categorical",
categorical_pipeline,
categorical_features,
),
(
"date",
date_pipeline,
date_feature,
),
],
remainder="drop",
)
model = Pipeline(
steps=[
("features", preprocessor),
(
"classifier",
LogisticRegression(
max_iter=2000,
class_weight="balanced",
),
),
]
)
model.fit(X_train, y_train)
predictions = model.predict_proba(X_valid)[:, 1]
이 구조의 장점은 다음과 같습니다.
- 훈련과 추론에 같은 변환 적용
- 교차검증 시 각 훈련 폴드에서만 대체값과 스케일 계산
- 신규 범주 처리
- 모델과 전처리 동시 저장
- 하이퍼파라미터 탐색 가능
scikit-learn 공식 문서도 Pipeline을 특성 선택, 정규화, 예측기를 순차적으로 연결하고 전체 단계의 파라미터를 함께 탐색하는 구조로 설명합니다.
출처: scikit-learn — Pipeline, Preprocessing data
18. 사용자 행동 데이터 실전 예제
고객 이탈 예측용 특성을 만든다고 가정하겠습니다.
18-1. 기준 시점 정의
prediction_time = pd.Timestamp(
"2026-07-01 00:00:00",
tz="UTC",
)
18-2. 과거 이벤트만 사용
history = events[
events["event_time"] < prediction_time
].copy()
18-3. 최근 기간 집계
def aggregate_window(
events: pd.DataFrame,
prediction_time: pd.Timestamp,
days: int,
) -> pd.DataFrame:
start_time = prediction_time - pd.Timedelta(days=days)
window = events[
(events["event_time"] >= start_time)
& (events["event_time"] < prediction_time)
]
return (
window.groupby("customer_id")
.agg(
event_count=("event_id", "count"),
active_days=("event_date", "nunique"),
purchase_count=("is_purchase", "sum"),
total_amount=("amount", "sum"),
average_amount=("amount", "mean"),
)
.add_suffix(f"_{days}d")
)
18-4. 여러 기간 결합
features_7d = aggregate_window(
history,
prediction_time,
days=7,
)
features_30d = aggregate_window(
history,
prediction_time,
days=30,
)
features = features_7d.join(
features_30d,
how="outer",
).fillna(0)
18-5. 변화율 추가
features["event_activity_ratio"] = (
features["event_count_7d"]
/ features["event_count_30d"].replace(0, np.nan)
)
features["purchase_amount_per_event_30d"] = (
features["total_amount_30d"]
/ features["event_count_30d"].replace(0, np.nan)
)
18-6. 최근성 추가
last_event = (
history.groupby("customer_id")["event_time"]
.max()
)
features["days_since_last_event"] = (
prediction_time - last_event
).dt.total_seconds() / 86400
이 방식은 하나의 기준 시점에 대한 스냅샷을 만듭니다. 여러 학습 행을 만들려면 고객별 여러 기준 시점을 생성하고, 각 시점마다 동일한 과거 전용 계산을 수행해야 합니다.
19. 특성 추출과 PCA
특성 선택과 특성 추출은 모두 차원을 줄일 수 있지만 접근 방식이 다릅니다.
| 구분 | 특성 선택 | 특성 추출 |
|---|---|---|
| 핵심 | 기존 특성 중 일부를 선택 | 기존 특성을 조합해 새로운 표현 생성 |
| 원본 특성 | 유지됨 | 새로운 축이나 벡터로 변환됨 |
| 해석 가능성 | 상대적으로 높음 | 상대적으로 낮음 |
| 대표 기법 | 분산 기준, RFE, L1 규제 | PCA, LDA, SVD, 오토인코더 |
| 주된 목적 | 잡음과 중복 제거 | 정보 압축과 표현 변환 |
다항식 특성, 임베딩, 주성분도 넓은 의미의 특성 추출에 포함됩니다.
19-1. PCA란 무엇인가
PCA(Principal Component Analysis, 주성분 분석)는 서로 상관된 여러 수치형 특성을 데이터 분산이 큰 새로운 축으로 투영하는 선형 차원 축소 기법입니다.
원본 데이터 행렬을 중심화한 뒤 다음과 같은 새로운 좌표계로 변환합니다.
$$ \mathbf{Z} = \mathbf{X}\mathbf{W} $$
여기서 \(\mathbf{W}\)의 각 열은 주성분 방향입니다.
- 첫 번째 주성분은 데이터 분산이 가장 큰 방향입니다.
- 두 번째 주성분은 첫 번째 주성분과 직교하면서 남은 분산을 가장 크게 설명합니다.
- 이후 주성분도 앞선 축과 직교합니다.
PCA의 목적은 타깃 예측력을 직접 최대화하는 것이 아니라 입력 데이터의 분산을 가능한 한 많이 보존하는 것입니다. 따라서 설명 분산이 높아도 예측 성능이 반드시 좋아지는 것은 아닙니다.
19-2. PCA 전에 표준화해야 하는 이유
PCA는 분산이 큰 방향을 우선 선택합니다. 단위가 서로 다르면 값의 범위가 큰 특성이 주성분을 지배할 수 있습니다.
예를 들어 연봉은 수천만 단위이고 나이는 수십 단위라면, 의미가 아니라 단위 차이 때문에 연봉 방향이 강조될 수 있습니다.
from sklearn.decomposition import PCA
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
pca_pipeline = Pipeline(
steps=[
("scaler", StandardScaler()),
("pca", PCA(n_components=0.95)),
]
)
X_reduced = pca_pipeline.fit_transform(X_train)
n_components=0.95는 누적 설명 분산이 95% 이상이 되도록 주성분 수를 자동으로 선택합니다.
다만 모든 상황에서 표준화가 정답은 아닙니다. 모든 특성이 동일한 단위이고 실제 분산 크기 자체가 의미 있다면 원본 공분산 구조를 유지할 수도 있습니다.
19-3. 설명 분산과 주성분 수 선택
각 주성분이 보존하는 분산 비율은 explained_variance_ratio_로 확인할 수 있습니다.
import numpy as np
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
X_scaled = StandardScaler().fit_transform(X_train)
pca = PCA()
pca.fit(X_scaled)
cumulative_ratio = np.cumsum(
pca.explained_variance_ratio_
)
n_components_95 = (
np.argmax(cumulative_ratio >= 0.95) + 1
)
print("95% 설명 분산에 필요한 주성분:", n_components_95)
주성분 수를 고를 때는 다음을 함께 봅니다.
- 누적 설명 분산
- 스크리 플롯의 꺾이는 지점
- 교차검증 성능
- 학습·추론 시간
- 저장 공간
- 해석 가능성
설명 분산 95%는 관례적인 기준일 뿐 절대 규칙은 아닙니다.
19-4. PCA를 파이프라인 안에 넣기
PCA도 데이터에서 축을 학습하므로 전체 데이터에 먼저 적용하면 검증 정보가 누수됩니다.
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
model = Pipeline(
steps=[
("scaler", StandardScaler()),
("pca", PCA(n_components=0.95)),
(
"classifier",
LogisticRegression(max_iter=2000),
),
]
)
model.fit(X_train, y_train)
교차검증에서는 각 훈련 폴드에서 표준화 통계와 PCA 축을 다시 학습해야 합니다.
19-5. PCA가 유용한 경우
- 수치형 특성이 매우 많고 상관관계가 강한 경우
- 센서·스펙트럼·픽셀처럼 고차원 연속값을 압축할 때
- 모델 학습 속도와 메모리를 줄여야 할 때
- 2차원이나 3차원으로 데이터 구조를 시각화할 때
- 다중공선성을 줄여야 할 때
19-6. PCA가 적합하지 않은 경우
- 원본 특성의 의미와 설명 가능성이 중요한 경우
- 범주형 원-핫 특성이 대부분인 경우
- 비선형 구조가 핵심인 경우
- 희소 행렬의 희소성을 유지해야 하는 경우
- 소수의 원본 특성만으로도 충분한 경우
- 주성분 계산 비용이 운영 이점을 넘는 경우
텍스트 TF-IDF처럼 희소한 행렬에는 중심화를 요구하지 않는 TruncatedSVD가 더 적합할 수 있습니다.
19-7. PCA의 흔한 오해
오해 1. PCA를 적용하면 성능이 항상 좋아진다
PCA는 타깃을 사용하지 않는 비지도 변환입니다. 분산이 작은 방향에 예측에 중요한 신호가 있을 수도 있으므로 반드시 교차검증으로 비교해야 합니다.
오해 2. 설명 분산이 높으면 정보가 완전히 보존된다
설명 분산은 입력 데이터의 전체 변동을 얼마나 보존하는지를 뜻합니다. 타깃과 관련된 정보 보존율과 동일하지 않습니다.
오해 3. PCA는 특성 선택이다
PCA는 원본 특성 일부를 남기는 것이 아니라 여러 특성을 섞어 새로운 주성분을 만드는 특성 추출입니다.
20. 딥러닝 시대에도 특성 공학이 필요한가
딥러닝은 이미지, 음성, 텍스트에서 표현 학습을 자동화했습니다. 그러나 특성 공학이 사라진 것은 아닙니다.
여전히 중요한 영역
- 정형 데이터
- 데이터가 적은 문제
- 산업 센서와 시계열
- 설명 가능성이 필요한 모델
- 실시간 시스템
- 여러 데이터 소스 결합
- 학습 비용이 제한된 환경
- 규칙과 도메인 지식이 강한 문제
딥러닝에서의 특성 공학
딥러닝에서도 다음 설계가 필요합니다.
- 입력 정규화
- 토큰화
- 시간 위치 표현
- 패치 크기
- 스펙트로그램 변환
- 로그 멜 특성
- 데이터 증강
- 마스킹
- 범주 임베딩
- 멀티모달 정렬
- 샘플링 전략
즉, 사람이 직접 최종 표현을 모두 설계하지 않을 뿐, 모델이 표현을 학습하기 좋은 입력 구조를 설계하는 일은 계속 필요합니다.
21. 자동 특성 공학과 Feature Store
20-1. 자동 특성 생성
일부 도구는 관계형 데이터의 집계, 시간 윈도우, 조합 특성을 자동 생성합니다.
장점:
- 후보 탐색 속도 향상
- 반복 작업 감소
- 기준 모델 구축에 유용
한계:
- 특성 수 폭증
- 계산 비용 증가
- 누수 위험
- 도메인 의미 부족
- 운영하기 어려운 특성 생성
자동 생성 도구를 사용해도 예측 시점, 데이터 가용성, 검증 설계는 사람이 책임져야 합니다.
20-2. Feature Store
Feature Store는 특성을 중앙에서 정의·저장·재사용하는 시스템입니다.
주요 목적:
- 학습과 서빙 일관성
- 특성 재사용
- 시점 기반 조회
- 버전 관리
- 계보 추적
- 품질 모니터링
- 온라인·오프라인 저장소 연동
Feature Store를 도입한다고 누수가 자동으로 해결되는 것은 아닙니다. 시점 정확 조회(point-in-time correct join)와 특성 정의가 올바르게 구현되어야 합니다.
22. 학습-서빙 불일치
학습에서는 정확했지만 운영에서 성능이 낮다면 특성 계산 차이를 확인해야 합니다.
대표 원인:
- 학습은 UTC, 운영은 한국시간
- 학습은 결측치를 중앙값 대체, 운영은 0 대체
- 학습은 30일 달력 구간, 운영은 720시간
- 학습은 취소 주문 제외, 운영은 포함
- 학습은 확정 데이터, 운영은 지연 도착 데이터
- 학습과 운영의 범주 사전이 다름
- 부동소수점 반올림 차이
- 외부 API 버전 변경
대응 방법
- 특성 계산 코드를 공통 라이브러리로 사용합니다.
- 동일 입력에 대해 배치와 온라인 결과를 비교하는 테스트를 만듭니다.
- 특성별 허용 범위를 검증합니다.
- 학습 데이터 생성 시점과 데이터 확정 지연을 기록합니다.
- 모델 버전과 특성 버전을 함께 배포합니다.
23. 특성 품질 모니터링
모델 성능만 모니터링하면 원인을 늦게 발견할 수 있습니다.
특성별로 다음을 추적합니다.
- 결측률
- 평균과 표준편차
- 분위수
- 최솟값·최댓값
- 범주 수
- 신규 범주 비율
- 0 비율
- 이상값 비율
- 갱신 지연
- 훈련 분포와의 차이
PSI 예시
Population Stability Index는 기준 분포와 현재 분포의 차이를 요약하는 지표입니다.
$$ \operatorname{PSI} = \sum_i (a_i – e_i) \ln\left(\frac{a_i}{e_i}\right) $$
여기서 \(e_i\)는 기준 구간 비율, \(a_i\)는 현재 구간 비율입니다.
PSI 하나만으로 드리프트를 판단하면 안 되며, KS 통계량, Wasserstein 거리, 범주별 변화, 모델 성능과 함께 봐야 합니다.
24. 자주 발생하는 실수
실수 1. ID를 그대로 넣는다
고객 ID, 주문 번호, 파일명은 특정 샘플을 외우게 만들 수 있습니다.
해결:
- ID 자체 대신 과거 행동 집계 사용
- 그룹 분할 적용
- 신규 ID 성능 별도 평가
실수 2. 전체 데이터에서 특성을 만든다
전체 기간 집계, 전체 평균, 전체 타깃 인코딩은 누수를 일으킵니다.
해결:
- 먼저 분할 전략 결정
- 훈련 데이터에서 변환기 학습
- 시계열은 기준 시점 이전만 사용
실수 3. 특성 수만 늘린다
상호작용과 다항식 특성을 무제한 추가하면 과적합과 비용이 증가합니다.
해결:
- 가설 기반 후보 생성
- 제거 실험
- 규제
- 특성 그룹별 성능 비교
실수 4. 중요도가 높으면 좋은 특성이라고 믿는다
워터마크, 서버 코드, 담당자 ID처럼 지름길을 학습했을 수 있습니다.
해결:
- 분포 외 평가
- 교란 실험
- 그룹별 평가
- 원인과 생성 시점 검토
실수 5. 운영 계산 가능성을 나중에 본다
오프라인에서만 계산 가능한 특성은 실제 서비스에 사용할 수 없습니다.
해결:
- 특성 설계 단계에서 데이터 지연과 계산 비용 포함
- 온라인 요구 시간을 정의
- 대체 가능한 근사 특성 준비
실수 6. 검증 세트에 맞춰 반복 선택한다
검증 결과를 보며 특성을 계속 수정하면 검증 세트에도 과적합됩니다.
해결:
- 최종 테스트 세트 봉인
- 중첩 교차검증 고려
- 실험 횟수와 결정 이력 기록
25. 실전 체크리스트
문제와 시점
- 타깃이 명확한가
- 예측 기준 시점이 정의되어 있는가
- 예측 시점에 사용 가능한 데이터만 쓰는가
- 데이터 확정 지연을 고려했는가
분할과 누수
- 특성 생성 전에 분할 전략을 정했는가
- 같은 고객·환자·장비·문서가 분할 간 겹치지 않는가
- 집계 윈도우가 미래를 포함하지 않는가
- 결측 대체·스케일링·인코딩을 훈련 데이터에서만 학습하는가
- 타깃 인코딩에 교차적합을 사용하는가
특성 설계
- 단위가 일관적인가
- 비율의 분모 0을 처리했는가
- 날짜와 시간대가 일관적인가
- 신규 범주를 처리하는가
- 희귀 범주를 관리하는가
- 결측 여부가 의미 있는지 확인했는가
- 장기 수준과 최근 변화를 함께 표현했는가
검증
- 기준 모델과 비교했는가
- 제거 실험을 했는가
- 여러 시드 또는 폴드에서 일관적인가
- 시간 외 검증을 했는가
- 그룹별 성능을 확인했는가
- 성능 향상 대비 운영 비용을 평가했는가
운영
- 학습과 서빙이 같은 코드를 사용하는가
- 특성 정의와 버전을 기록했는가
- 특성 품질 테스트가 있는가
- 결측률과 분포 변화를 모니터링하는가
- 모델과 특성 버전을 함께 배포하는가
26. FAQ
특성은 많을수록 좋은가
아닙니다. 충분한 데이터와 규제가 있다면 많은 특성이 도움이 될 수 있지만, 중복·잡음·누수 특성은 과적합과 운영 비용을 늘립니다. 성능과 비용을 기준으로 선택해야 합니다.
트리 모델에는 스케일링이 필요 없는가
의사결정나무 계열은 일반적으로 특성의 단조 스케일 변환에 민감하지 않습니다. 그러나 거리 기반 전처리, 다른 모델과의 결합, 수치 안정성, 특정 구현 조건 때문에 스케일링이 필요할 수 있습니다.
상관계수가 낮은 특성은 제거해도 되는가
단순 상관계수는 비선형 관계와 상호작용을 놓칠 수 있습니다. 상관이 낮다는 이유만으로 제거하지 말고 모델 기반 평가와 제거 실험을 함께 사용해야 합니다.
딥러닝은 특성 공학이 필요 없는가
원시 표현에서 많은 특성을 자동 학습하지만 입력 표현, 정규화, 시간 정보, 토큰화, 데이터 증강, 샘플링은 여전히 성능에 큰 영향을 줍니다.
가장 먼저 만들어야 할 특성은 무엇인가
문제마다 다르지만 실전에서는 다음 순서가 효율적입니다.
- 결측 여부
- 날짜·시간 분해
- 경과 시간
- 비율과 차이
- 최근 기간 집계
- 장기 대비 최근 변화
- 도메인 임계값과 상태
- 상호작용
특성 선택은 언제 해야 하는가
기준 파이프라인과 충분한 특성 후보를 만든 뒤 교차검증 안에서 수행합니다. 전체 데이터로 먼저 특성을 선택하면 검증 정보가 누수될 수 있습니다.
마무리
특성 공학은 단순히 열을 추가하는 기술이 아닙니다. 실제 문제의 구조, 예측 시점, 데이터 생성 과정, 모델의 학습 방식, 운영 환경을 하나의 입력 표현으로 연결하는 설계 작업입니다.
좋은 특성은 복잡한 모델보다 큰 성능 향상을 만들 수 있습니다. 그러나 높은 검증 점수만 보고 특성을 선택하면 타깃 누수와 지름길 학습을 놓칠 수 있습니다.
실전에서는 다음 원칙을 기억하면 됩니다.
예측 시점을 먼저 고정한다.
훈련 데이터만 보고 변환을 학습한다.
도메인 가설을 특성으로 표현한다.
같은 분할에서 제거 실험으로 검증한다.
학습과 운영의 계산을 일치시킨다.
특성 품질과 분포 변화를 계속 감시한다.
특성 공학의 목표는 모델에 더 많은 숫자를 넣는 것이 아니라, 미래에도 유지되는 유용한 신호를 안전하고 재현 가능한 방식으로 제공하는 것입니다.
참고문헌
- scikit-learn — Pipelines and composite estimators
- scikit-learn — Pipeline
- scikit-learn — ColumnTransformer
- scikit-learn — Preprocessing data
- scikit-learn — Feature selection
- scikit-learn — SequentialFeatureSelector
- scikit-learn — TargetEncoder
- scikit-learn — Common pitfalls and recommended practices
- pandas — Windowing operations
- pandas — Time series and date functionality
