이미지 로딩 중...
AI Generated
2025. 11. 21. · 8 Views
데이터 전처리와 피처 엔지니어링 완벽 가이드
머신러닝 모델의 성능을 좌우하는 데이터 전처리와 피처 엔지니어링의 핵심 개념을 실무 예제와 함께 배워봅니다. 결측치 처리부터 스케일링, 인코딩, 피처 생성까지 실전에서 바로 활용할 수 있는 기법들을 다룹니다.
목차
- 결측치 처리 - 빠진 데이터를 똑똑하게 다루는 방법
- 데이터 스케일링 - 서로 다른 단위의 데이터를 공정하게 비교하기
- 원-핫 인코딩 - 범주형 데이터를 숫자로 변환하기
- 이상치 탐지와 처리 - 극단적인 값을 똑똑하게 다루기
- 피처 생성 - 새로운 의미 있는 변수 만들기
- 차원 축소 - 많은 변수를 적은 변수로 압축하기
- 텍스트 데이터 전처리 - 문자를 숫자로 변환하기
- 시계열 데이터 피처 생성 - 시간의 흐름을 변수로 만들기
1. 결측치 처리 - 빠진 데이터를 똑똑하게 다루는 방법
시작하며
여러분이 고객 데이터를 분석하려는데, 어떤 고객은 나이 정보가 없고, 어떤 고객은 수입 정보가 빠져있는 상황을 겪어본 적 있나요? 실제 데이터는 항상 완벽하지 않습니다.
설문조사에서 일부 질문을 건너뛰거나, 센서가 일시적으로 오작동하거나, 시스템 오류로 데이터가 누락되는 경우가 정말 많습니다. 이런 결측치 문제는 실제 개발 현장에서 가장 먼저 마주하는 도전과제입니다.
결측치를 잘못 처리하면 머신러닝 모델이 학습조차 되지 않거나, 편향된 결과를 만들어낼 수 있습니다. 예를 들어, 고소득자들이 수입 정보를 공개하지 않는 경향이 있다면, 단순히 그 데이터를 삭제하면 모델이 저소득층에만 편향되는 문제가 생깁니다.
바로 이럴 때 필요한 것이 결측치 처리 기법입니다. 데이터의 특성과 결측치가 발생한 이유에 따라 적절한 방법을 선택하면, 데이터 손실을 최소화하면서도 모델의 성능을 크게 향상시킬 수 있습니다.
개요
간단히 말해서, 결측치 처리는 빠진 데이터를 채우거나 제거하는 과정입니다. 마치 퍼즐에서 몇 조각이 빠졌을 때, 그림의 맥락을 보고 유추하여 채우거나 그 부분을 제외하고 나머지로 전체 그림을 파악하는 것과 같습니다.
결측치 처리가 왜 중요할까요? 대부분의 머신러닝 알고리즘은 결측치가 있으면 에러를 발생시키거나 예측 성능이 크게 떨어집니다.
예를 들어, 부동산 가격 예측 모델을 만드는데 방 개수 정보가 30% 누락되어 있다면, 그냥 무시할 수 없는 문제입니다. 또한 결측치가 무작위로 발생한 것이 아니라 특정 패턴이 있다면(예: 고가 주택일수록 정보 공개를 꺼림), 이를 제대로 처리하지 않으면 모델이 잘못된 패턴을 학습하게 됩니다.
전통적으로는 결측치가 있는 행을 전부 삭제하거나 평균값으로 채우는 단순한 방법을 사용했습니다. 하지만 이제는 데이터의 분포와 관계를 고려한 더 정교한 방법들을 사용할 수 있습니다.
KNN 임퓨테이션, 다중 임퓨테이션, 딥러닝 기반 예측 등 다양한 기법이 있습니다. 결측치 처리의 핵심 특징은 첫째, 데이터 손실을 최소화한다는 점입니다.
둘째, 데이터의 통계적 특성을 보존합니다. 셋째, 결측치가 발생한 패턴 자체도 중요한 정보가 될 수 있습니다(예: 결측 여부를 나타내는 새로운 컬럼 생성).
이러한 특징들이 중요한 이유는 적은 양의 데이터로도 더 나은 모델을 만들 수 있고, 실제 데이터의 의미를 왜곡하지 않기 때문입니다.
코드 예제
import pandas as pd
import numpy as np
from sklearn.impute import SimpleImputer, KNNImputer
# 샘플 데이터 생성 - 일부 값은 의도적으로 결측치로 설정
df = pd.DataFrame({
'age': [25, 30, np.nan, 45, 50, np.nan, 35],
'salary': [50000, 60000, 55000, np.nan, 80000, 70000, np.nan],
'experience': [2, 5, 3, np.nan, 10, 8, 6]
})
# 방법 1: 평균값으로 채우기 - 가장 기본적인 방법
simple_imputer = SimpleImputer(strategy='mean')
df_mean = pd.DataFrame(
simple_imputer.fit_transform(df),
columns=df.columns
)
# 방법 2: KNN 임퓨터 - 유사한 데이터 포인트의 값으로 채우기
knn_imputer = KNNImputer(n_neighbors=2)
df_knn = pd.DataFrame(
knn_imputer.fit_transform(df),
columns=df.columns
)
# 방법 3: 결측치 여부를 새로운 피처로 추가 (결측 패턴이 중요한 경우)
df['age_missing'] = df['age'].isnull().astype(int)
df['salary_missing'] = df['salary'].isnull().astype(int)
print("원본 데이터:\n", df.head())
print("\n평균값 대체:\n", df_mean.head())
print("\nKNN 대체:\n", df_knn.head())
설명
이것이 하는 일: 위 코드는 실무에서 가장 많이 사용되는 세 가지 결측치 처리 방법을 보여줍니다. 각각의 방법은 서로 다른 상황에 적합하며, 데이터의 특성과 결측치의 패턴에 따라 선택할 수 있습니다.
첫 번째로, SimpleImputer는 가장 기본적인 방법으로 결측치를 평균값으로 대체합니다. 이 방법은 빠르고 간단하지만, 데이터의 변동성을 줄이는 단점이 있습니다.
예를 들어, 나이가 25, 30, 45, 50인 데이터에서 결측치를 평균인 37.5로 채우면, 실제로는 극단적인 값(매우 젊거나 나이 많은)일 수도 있는 가능성을 무시하게 됩니다. 하지만 결측치가 적고 무작위로 발생한 경우라면 충분히 효과적입니다.
두 번째로, KNNImputer는 더 정교한 방법입니다. 이것은 결측치가 있는 데이터 포인트와 가장 유사한 K개의 이웃을 찾아, 그들의 평균값으로 채웁니다.
예를 들어, 경력이 3년이고 나이가 비어있는 사람이 있다면, 경력이 비슷한(2년, 5년) 다른 사람들의 나이(25, 30)를 참고하여 약 27.5세로 추정하는 것입니다. 이 방법은 데이터 간의 관계를 보존하므로 더 현실적인 값을 만들어냅니다.
세 번째로, 결측치 여부 자체를 새로운 피처로 만드는 방법입니다. 때로는 "데이터가 없다"는 사실 자체가 중요한 정보입니다.
예를 들어, 급여 정보를 공개하지 않은 사람들이 실제로 고소득자일 가능성이 높다면, salary_missing 컬럼의 값이 1인 경우를 모델이 학습할 수 있습니다. 이렇게 하면 원본 데이터를 채운 후에도 결측 패턴 정보를 잃지 않습니다.
여러분이 이 코드를 사용하면 데이터 손실 없이 머신러닝 모델을 학습시킬 수 있습니다. 또한 결측치 처리 방법에 따라 모델 성능이 5-15% 정도 차이날 수 있으므로, 여러 방법을 실험해보는 것이 좋습니다.
특히 KNN 방법은 데이터 간 관계가 명확한 경우(나이와 경력, 가격과 크기 등) 매우 효과적입니다. 실무에서는 보통 여러 방법을 조합합니다.
수치형 데이터는 KNN으로, 범주형 데이터는 최빈값으로 채우고, 동시에 결측 여부 플래그도 추가하는 식입니다. 이렇게 하면 각 방법의 장점을 모두 활용할 수 있습니다.
실전 팁
💡 결측치 비율이 70% 이상인 컬럼은 아예 삭제하는 것이 좋습니다. 그만큼 많은 데이터가 없다면 임퓨테이션으로 채워도 신뢰도가 낮기 때문입니다.
💡 KNN 임퓨터를 사용하기 전에는 반드시 데이터를 스케일링해야 합니다. 그렇지 않으면 큰 값(예: 연봉 50000)이 작은 값(예: 나이 30)보다 거리 계산에 지나치게 큰 영향을 미칩니다.
💡 시계열 데이터의 결측치는 forward fill이나 backward fill을 사용하세요. 시간 순서가 중요한 데이터에서는 바로 이전 값이나 이후 값으로 채우는 것이 평균보다 더 의미 있습니다.
💡 결측치가 완전히 무작위(MCAR)인지, 관측 가능한 변수와 관련(MAR)이 있는지, 관측 불가능한 이유(MNAR)인지 먼저 분석하세요. 패턴에 따라 적절한 처리 방법이 달라집니다.
💡 프로덕션 환경에서는 학습 데이터로 fit한 임퓨터를 저장해두고, 새로운 데이터에는 transform만 적용하세요. 이렇게 해야 학습과 추론 시 같은 기준으로 결측치를 처리할 수 있습니다.
2. 데이터 스케일링 - 서로 다른 단위의 데이터를 공정하게 비교하기
시작하며
여러분이 아파트 가격 예측 모델을 만든다고 상상해보세요. 한 변수는 방 개수(2, 3, 4 같은 작은 숫자)이고, 다른 변수는 가격(200,000,000원 같은 큰 숫자)입니다.
이 두 변수를 그대로 모델에 넣으면 어떻게 될까요? 가격이라는 큰 숫자가 방 개수라는 작은 숫자를 완전히 압도해버려서, 모델은 방 개수의 중요성을 제대로 학습하지 못하게 됩니다.
이런 문제는 거리 기반 알고리즘(KNN, K-Means)이나 경사하강법을 사용하는 알고리즘(신경망, 로지스틱 회귀)에서 특히 심각합니다. 변수들의 스케일이 다르면 학습 속도가 느려지고, 최적의 해를 찾지 못하거나, 어떤 변수가 실제로 중요한지 판단하기 어렵습니다.
마치 키를 센티미터로 재는 사람과 미터로 재는 사람이 "누가 더 크냐"를 비교하려는 것과 같은 혼란이 발생합니다. 바로 이럴 때 필요한 것이 데이터 스케일링입니다.
모든 변수를 비슷한 범위로 조정하면, 모델이 각 변수의 진짜 중요도를 공정하게 평가할 수 있고, 학습도 훨씬 빠르고 안정적으로 진행됩니다.
개요
간단히 말해서, 스케일링은 서로 다른 범위를 가진 변수들을 같은 척도로 변환하는 과정입니다. 마치 미터, 센티미터, 킬로미터를 모두 미터로 통일하는 것처럼, 데이터를 일관된 기준으로 맞춰주는 것입니다.
왜 스케일링이 필요할까요? 첫째, 많은 머신러닝 알고리즘은 변수 간의 거리나 크기를 계산합니다.
KNN은 유사도를 거리로 측정하고, 신경망은 가중치를 업데이트할 때 변수의 크기에 영향을 받습니다. 예를 들어, 신용 점수(300-850 범위)와 연봉(2000만원-1억원 범위)을 함께 사용하면, 연봉의 변화가 모델에 훨씬 큰 영향을 미치게 됩니다.
둘째, 스케일링을 하면 학습이 빨라집니다. 경사하강법에서 모든 변수의 스케일이 비슷하면 최적점을 향해 더 직선적으로 수렴할 수 있습니다.
전통적으로는 모든 값을 0-1 사이로 변환하는 Min-Max 스케일링을 많이 사용했습니다. 하지만 이제는 데이터의 분포에 따라 표준화(Standardization), 로버스트 스케일링(Robust Scaling) 등 다양한 방법을 선택할 수 있습니다.
특히 이상치가 많은 데이터라면 Min-Max보다는 Robust Scaler가 더 효과적입니다. 스케일링의 핵심 특징은 첫째, 데이터의 분포 형태는 바뀌지 않고 범위만 조정된다는 점입니다.
둘째, 변수 간의 상대적인 관계는 유지됩니다. 셋째, 스케일링 방법에 따라 이상치에 대한 민감도가 다릅니다.
이러한 특징들이 중요한 이유는 원본 데이터의 본질적인 패턴은 보존하면서도, 모델이 학습하기 좋은 형태로 데이터를 변환하기 때문입니다.
코드 예제
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
import numpy as np
import pandas as pd
# 샘플 데이터 - 스케일이 크게 다른 변수들
df = pd.DataFrame({
'age': [25, 30, 35, 40, 45],
'salary': [30000000, 45000000, 60000000, 75000000, 90000000],
'credit_score': [650, 700, 750, 800, 850]
})
# 방법 1: 표준화 (평균 0, 표준편차 1) - 가장 많이 사용됨
standard_scaler = StandardScaler()
df_standard = pd.DataFrame(
standard_scaler.fit_transform(df),
columns=df.columns
)
# 방법 2: Min-Max 스케일링 (0-1 범위로 변환)
minmax_scaler = MinMaxScaler()
df_minmax = pd.DataFrame(
minmax_scaler.fit_transform(df),
columns=df.columns
)
# 방법 3: Robust 스케일링 (중앙값과 IQR 사용, 이상치에 강함)
robust_scaler = RobustScaler()
df_robust = pd.DataFrame(
robust_scaler.fit_transform(df),
columns=df.columns
)
print("원본 데이터:\n", df)
print("\n표준화:\n", df_standard)
print("\nMin-Max:\n", df_minmax)
print("\nRobust:\n", df_robust)
설명
이것이 하는 일: 위 코드는 실무에서 가장 많이 사용되는 세 가지 스케일링 방법을 보여줍니다. 나이(25-45), 연봉(3천만원-9천만원), 신용점수(650-850)처럼 범위가 완전히 다른 변수들을 같은 척도로 변환하는 과정입니다.
첫 번째로, StandardScaler는 각 변수의 평균을 0, 표준편차를 1로 만듭니다. 공식은 (값 - 평균) / 표준편차입니다.
예를 들어, 나이의 평균이 35세, 표준편차가 7.9라면, 25세는 약 -1.27, 45세는 약 +1.27로 변환됩니다. 이 방법은 정규분포에 가까운 데이터에 가장 효과적이며, 딥러닝과 로지스틱 회귀 같은 알고리즘에서 거의 필수적으로 사용됩니다.
변환 후 대부분의 값이 -3에서 +3 사이에 위치하게 되어, 모든 변수가 비슷한 영향력을 갖게 됩니다. 두 번째로, MinMaxScaler는 모든 값을 0과 1 사이로 변환합니다.
공식은 (값 - 최소값) / (최대값 - 최소값)입니다. 나이의 경우 최소 25, 최대 45이므로, 25세는 0, 35세는 0.5, 45세는 1이 됩니다.
이 방법은 값의 범위가 고정되어야 하는 경우(예: 이미지 픽셀 값 0-255를 0-1로)나, 신경망의 입력층에서 자주 사용됩니다. 하지만 이상치에 매우 민감하다는 단점이 있습니다.
만약 연봉 데이터에 10억원이 하나 있다면, 대부분의 정상 데이터(3천만-9천만)가 0에 가까운 값으로 압축되어버립니다. 세 번째로, RobustScaler는 중앙값과 IQR(사분위수 범위)을 사용합니다.
공식은 (값 - 중앙값) / IQR입니다. 평균과 표준편차 대신 중앙값과 사분위수를 사용하기 때문에 이상치의 영향을 덜 받습니다.
실무에서 금융 데이터나 센서 데이터처럼 이상치가 자주 발생하는 경우에 매우 유용합니다. 예를 들어, 대부분의 사람이 3-9천만원을 벌지만 몇몇 CEO가 수십억을 버는 데이터라면, Robust Scaler가 일반적인 패턴을 더 잘 보존합니다.
여러분이 이 코드를 사용하면 모델의 학습 속도가 2-10배 빨라질 수 있고, 정확도도 5-20% 향상될 수 있습니다. 특히 신경망에서는 스케일링 여부가 모델이 수렴하느냐 발산하느냐를 결정할 정도로 중요합니다.
또한 PCA나 K-Means 같은 거리 기반 알고리즘에서는 스케일링이 필수입니다. 실무에서는 학습 데이터로 scaler를 fit한 후, 검증 데이터와 테스트 데이터에는 transform만 적용해야 합니다.
그렇지 않으면 데이터 누수(data leakage)가 발생하여 모델 성능이 과대평가됩니다. 또한 트리 기반 모델(Random Forest, XGBoost)은 스케일링이 필요 없지만, 선형 모델과 거리 기반 모델에서는 거의 필수입니다.
실전 팁
💡 신경망을 사용한다면 거의 항상 StandardScaler를 사용하세요. 가중치 초기화와 경사하강법 모두 데이터가 평균 0, 분산 1 근처에 있을 때 가장 잘 작동합니다.
💡 이미지 데이터는 보통 255로 나누어 0-1 범위로 만듭니다. 이것도 일종의 Min-Max 스케일링이며, 픽셀 값이 이미 0-255로 고정되어 있어 이상치 문제가 없기 때문입니다.
💡 학습 데이터로만 scaler를 fit하고, 테스트 데이터는 transform만 하세요. 예: scaler.fit(X_train) → scaler.transform(X_test). 테스트 데이터를 fit에 포함시키면 미래 정보가 누수됩니다.
💡 범주형 변수를 인코딩한 후에는 스케일링하지 마세요. One-hot 인코딩된 0과 1은 이미 같은 스케일이며, 스케일링하면 해석이 어려워집니다.
💡 이상치가 많은 금융 데이터, 센서 데이터라면 RobustScaler를 먼저 시도해보세요. 평균과 표준편차는 극단값 하나에 크게 영향받지만, 중앙값과 IQR은 안정적입니다.
3. 원-핫 인코딩 - 범주형 데이터를 숫자로 변환하기
시작하며
여러분이 과일 판매 데이터를 분석한다고 해볼까요? 데이터에 "사과", "바나나", "오렌지" 같은 텍스트가 있는데, 머신러닝 모델은 숫자만 이해할 수 있습니다.
가장 간단한 방법은 사과=1, 바나나=2, 오렌지=3으로 숫자를 부여하는 것입니다. 하지만 이렇게 하면 큰 문제가 생깁니다.
모델이 "오렌지(3)가 사과(1)보다 2배 크다"거나 "바나나는 사과와 오렌지의 중간"이라고 잘못 학습할 수 있습니다. 이런 문제는 범주형 데이터를 다룰 때 반드시 발생합니다.
색상(빨강, 파랑, 노랑), 지역(서울, 부산, 대구), 직업(의사, 교사, 엔지니어) 같은 데이터는 순서나 크기 개념이 없는데, 단순히 숫자로 변환하면 모델이 존재하지 않는 순서 관계를 학습하게 됩니다. 실제로 이렇게 잘못 인코딩하면 모델 성능이 크게 떨어지거나, 완전히 잘못된 예측을 할 수 있습니다.
바로 이럴 때 필요한 것이 원-핫 인코딩입니다. 각 범주를 독립적인 이진 변수로 만들어서, 모델이 범주 간에 잘못된 관계를 학습하는 것을 방지합니다.
개요
간단히 말해서, 원-핫 인코딩은 하나의 범주형 변수를 여러 개의 이진(0 또는 1) 변수로 쪼개는 방법입니다. 마치 "좋아하는 과일이 뭐예요?"라는 질문 하나를 "사과를 좋아하나요?
(예/아니오)", "바나나를 좋아하나요? (예/아니오)", "오렌지를 좋아하나요?
(예/아니오)" 세 개의 질문으로 나누는 것과 같습니다. 왜 원-핫 인코딩이 필요할까요?
머신러닝 모델은 기본적으로 숫자 간의 관계를 학습합니다. 만약 "서울"을 1, "부산"을 2로 인코딩하면, 모델은 부산이 서울보다 "더 크다"고 잘못 이해할 수 있습니다.
특히 선형 모델이나 신경망에서는 이런 잘못된 관계가 가중치에 직접 반영되어 예측 오류를 발생시킵니다. 예를 들어, 지역별 부동산 가격을 예측하는데 지역을 1, 2, 3으로 인코딩하면, 모델이 "지역 번호가 1 증가하면 가격이 X원 증가한다"는 엉뚱한 규칙을 학습하게 됩니다.
전통적인 레이블 인코딩(사과=1, 바나나=2, 오렌지=3)은 순서가 있는 범주형 데이터(예: 학점 A>B>C>D)에만 적합합니다. 하지만 원-핫 인코딩을 사용하면 순서 없는 범주형 데이터도 올바르게 처리할 수 있습니다.
사과가 [1,0,0], 바나나가 [0,1,0], 오렌지가 [0,0,1]이 되어, 각 범주가 완전히 독립적으로 표현됩니다. 원-핫 인코딩의 핵심 특징은 첫째, 각 범주가 독립적인 차원을 가진다는 점입니다.
둘째, 한 번에 하나의 범주만 1이고 나머지는 0입니다. 셋째, 범주의 개수만큼 새로운 컬럼이 생성됩니다.
이러한 특징들이 중요한 이유는 모델이 각 범주의 영향을 독립적으로 학습할 수 있고, 범주 간에 잘못된 순서 관계가 생기지 않기 때문입니다.
코드 예제
import pandas as pd
from sklearn.preprocessing import OneHotEncoder
# 샘플 데이터 - 범주형 변수들
df = pd.DataFrame({
'city': ['서울', '부산', '서울', '대구', '부산'],
'fruit': ['사과', '바나나', '오렌지', '사과', '바나나'],
'size': ['S', 'M', 'L', 'M', 'S']
})
# 방법 1: pandas의 get_dummies 사용 - 가장 간단
df_encoded = pd.get_dummies(df, columns=['city', 'fruit', 'size'])
# 방법 2: sklearn의 OneHotEncoder 사용 - 더 많은 제어 가능
encoder = OneHotEncoder(sparse_output=False, drop='first') # drop='first'로 다중공선성 방지
encoded_array = encoder.fit_transform(df[['city', 'fruit', 'size']])
# 컬럼 이름 생성
feature_names = encoder.get_feature_names_out(['city', 'fruit', 'size'])
df_sklearn = pd.DataFrame(encoded_array, columns=feature_names)
print("원본 데이터:\n", df)
print("\npandas get_dummies 결과:\n", df_encoded)
print("\nsklearn OneHotEncoder 결과:\n", df_sklearn)
print("\n인코딩된 컬럼명:", feature_names)
# 새로운 데이터에 같은 인코딩 적용 (프로덕션 환경)
new_data = pd.DataFrame({'city': ['서울'], 'fruit': ['사과'], 'size': ['L']})
new_encoded = encoder.transform(new_data)
print("\n새 데이터 인코딩:\n", new_encoded)
설명
이것이 하는 일: 위 코드는 "서울", "부산" 같은 텍스트 범주를 0과 1로 이루어진 여러 컬럼으로 변환합니다. 예를 들어, '서울'이라는 하나의 값이 'city_서울=1, city_부산=0, city_대구=0'이라는 세 개의 값으로 분리됩니다.
첫 번째로, pandas의 get_dummies는 가장 간단한 방법입니다. 단 한 줄의 코드로 모든 범주형 컬럼을 자동으로 인코딩합니다.
예를 들어, 'city' 컬럼이 ['서울', '부산', '대구'] 세 가지 값을 가지면, 'city_서울', 'city_부산', 'city_대구' 세 개의 새로운 컬럼이 생성됩니다. '서울' 데이터는 [1,0,0], '부산'은 [0,1,0], '대구'는 [0,0,1]로 표현됩니다.
이 방법은 빠르고 직관적이지만, 학습 데이터와 테스트 데이터에서 범주가 다르면 문제가 생길 수 있습니다. 두 번째로, sklearn의 OneHotEncoder는 더 강력하고 프로덕션 환경에 적합합니다.
이것은 학습 데이터로 fit한 후 저장했다가, 나중에 새로운 데이터에 동일한 변환을 적용할 수 있습니다. drop='first' 옵션을 사용하면 첫 번째 범주를 제거하여 다중공선성 문제를 방지합니다.
예를 들어, 'city_서울'과 'city_부산'만 있어도 대구인지 알 수 있습니다(둘 다 0이면 대구). 이렇게 하면 변수 개수가 줄어들고 모델의 안정성이 높아집니다.
세 번째 특징은 새로운 데이터 처리입니다. 학습 때 본 적 없는 범주가 새로 들어오면 어떻게 할까요?
OneHotEncoder는 handle_unknown='ignore' 옵션을 사용하면 모든 값을 0으로 설정합니다. 또는 학습 데이터에 있는 범주만 인코딩하고, 없는 범주는 '기타'로 묶을 수도 있습니다.
이런 유연성 덕분에 실제 서비스에서 예상치 못한 데이터가 들어와도 에러 없이 처리할 수 있습니다. 여러분이 이 코드를 사용하면 범주형 데이터를 올바르게 모델에 입력할 수 있어, 정확도가 크게 향상됩니다.
실제로 범주형 변수를 잘못 인코딩하면 모델 성능이 10-30% 떨어질 수 있습니다. 특히 선형 회귀, 로지스틱 회귀, 신경망 같은 모델에서는 원-핫 인코딩이 거의 필수입니다.
하지만 주의할 점도 있습니다. 범주가 수백 개라면(예: 우편번호, 사용자 ID) 원-핫 인코딩은 비효율적입니다.
이 경우 타겟 인코딩, 임베딩 등 다른 방법을 고려해야 합니다. 또한 트리 기반 모델(랜덤 포레스트, XGBoost)은 범주형 데이터를 직접 처리할 수 있어 원-핫 인코딩이 꼭 필요하지 않을 수도 있습니다.
실전 팁
💡 범주가 100개 이상이면 원-핫 인코딩 대신 타겟 인코딩이나 임베딩을 고려하세요. 너무 많은 컬럼이 생기면 메모리 문제와 차원의 저주가 발생합니다.
💡 drop='first' 옵션을 사용하여 하나의 범주를 제거하세요. 이렇게 하면 다중공선성을 피할 수 있고, 특히 선형 모델에서 더 안정적인 결과를 얻을 수 있습니다.
💡 프로덕션 환경에서는 반드시 encoder를 저장(pickle)하여 재사용하세요. 학습 때와 추론 때 같은 인코딩 규칙을 적용해야 일관성이 유지됩니다.
💡 희소 범주(빈도가 5% 미만)는 '기타(Other)' 범주로 묶는 것을 고려하세요. 너무 드문 범주는 모델 학습에 도움이 안 되고 과적합을 유발할 수 있습니다.
💡 순서가 있는 범주(저/중/고, S/M/L)는 원-핫 인코딩 대신 순서형 인코딩(1,2,3)을 사용하는 것이 더 효과적일 수 있습니다. 순서 정보를 보존하면 모델이 패턴을 더 쉽게 학습합니다.
4. 이상치 탐지와 처리 - 극단적인 값을 똑똑하게 다루기
시작하며
여러분이 직원들의 연봉 데이터를 분석하는데, 대부분은 3천만원에서 7천만원 사이인데 CEO 한 명이 30억원을 받는다면 어떻게 될까요? 평균 연봉을 계산하면 실제 대부분의 직원 급여와는 동떨어진 왜곡된 값이 나옵니다.
머신러닝 모델도 마찬가지로 이런 극단적인 값에 과도하게 영향을 받아 잘못된 패턴을 학습할 수 있습니다. 이런 이상치는 실무에서 정말 흔하게 발생합니다.
센서 오작동으로 온도가 999도로 기록되거나, 데이터 입력 실수로 나이가 200세로 들어가거나, 실제로 극단적인 케이스(초고가 부동산)가 존재할 수도 있습니다. 문제는 이상치가 모델의 학습을 방해하고, 평균이나 표준편차 같은 통계량을 왜곡시키며, 모델의 일반화 성능을 크게 떨어뜨린다는 것입니다.
바로 이럴 때 필요한 것이 이상치 탐지와 처리 기법입니다. 이상치를 올바르게 찾아내고, 제거하거나 변환하거나 캡핑하면, 모델이 정상적인 패턴에 집중할 수 있고 예측 성능도 크게 향상됩니다.
개요
간단히 말해서, 이상치 탐지는 대부분의 데이터와 크게 동떨어진 극단값을 찾아내는 과정입니다. 마치 교실에서 대부분의 학생 키가 150-170cm인데 유독 한 명이 200cm라면 "특이한 케이스"로 인식하는 것과 같습니다.
왜 이상치 처리가 중요할까요? 첫째, 통계적 왜곡을 방지합니다.
평균과 표준편차는 이상치에 매우 민감하여, 단 하나의 극단값이 전체 분포를 대표하는 통계량을 크게 바꿀 수 있습니다. 둘째, 모델 성능을 개선합니다.
선형 회귀 같은 모델은 이상치에 과도하게 맞추려다가 정상 데이터의 패턴을 놓칠 수 있습니다. 예를 들어, 부동산 가격 예측 모델에서 100억짜리 펜트하우스 몇 개가 일반 아파트 가격 예측을 망칠 수 있습니다.
셋째, 데이터 품질 문제를 발견합니다. 이상치를 조사하다 보면 데이터 입력 오류나 시스템 문제를 발견할 수 있습니다.
전통적으로는 평균에서 3 표준편차 이상 떨어진 값을 이상치로 간주했습니다. 하지만 이제는 IQR(사분위수 범위), Z-score, Isolation Forest, Local Outlier Factor 같은 더 정교한 방법들을 사용합니다.
특히 다차원 데이터에서는 각 변수별로는 정상이지만 변수들의 조합이 이상한 경우도 탐지할 수 있습니다. 이상치 처리의 핵심 특징은 첫째, 이상치가 오류인지 진짜 극단값인지 구분해야 한다는 점입니다.
둘째, 제거, 변환(로그), 캡핑(상한/하한 설정) 등 다양한 처리 방법이 있습니다. 셋째, 도메인 지식이 매우 중요합니다.
이러한 특징들이 중요한 이유는 무조건 이상치를 제거하는 것이 아니라, 데이터의 맥락을 고려하여 적절히 처리해야 하기 때문입니다.
코드 예제
import numpy as np
import pandas as pd
from scipy import stats
from sklearn.ensemble import IsolationForest
# 샘플 데이터 - 이상치 포함
np.random.seed(42)
normal_data = np.random.normal(50, 10, 100) # 정상 데이터
outliers = np.array([150, 200, -50]) # 이상치
data = np.concatenate([normal_data, outliers])
df = pd.DataFrame({'value': data})
# 방법 1: IQR 방법 (사분위수 범위) - 가장 견고함
Q1 = df['value'].quantile(0.25)
Q3 = df['value'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR # 하한
upper_bound = Q3 + 1.5 * IQR # 상한
df['is_outlier_iqr'] = (df['value'] < lower_bound) | (df['value'] > upper_bound)
# 방법 2: Z-score 방법 - 정규분포 가정
z_scores = np.abs(stats.zscore(df['value']))
df['is_outlier_zscore'] = z_scores > 3 # 3 표준편차 초과
# 방법 3: Isolation Forest - 머신러닝 기반, 다차원 데이터에 강함
iso_forest = IsolationForest(contamination=0.03, random_state=42) # 3% 이상치 예상
df['is_outlier_iforest'] = iso_forest.fit_predict(df[['value']]) == -1
# 이상치 처리 예시
df_cleaned = df[~df['is_outlier_iqr']].copy() # 제거
df['value_capped'] = df['value'].clip(lower_bound, upper_bound) # 캡핑
print("원본 데이터 통계:\n", df['value'].describe())
print(f"\nIQR 기준 이상치: {df['is_outlier_iqr'].sum()}개")
print(f"Z-score 기준 이상치: {df['is_outlier_zscore'].sum()}개")
print(f"Isolation Forest 이상치: {df['is_outlier_iforest'].sum()}개")
print("\n이상치 제거 후 통계:\n", df_cleaned['value'].describe())
설명
이것이 하는 일: 위 코드는 세 가지 다른 방법으로 이상치를 탐지하고, 두 가지 방법으로 처리하는 과정을 보여줍니다. 평균 50, 표준편차 10인 정상 데이터 100개에 극단값 150, 200, -50을 섞어서 실제 상황을 시뮬레이션합니다.
첫 번째로, IQR 방법은 가장 견고하고 널리 사용됩니다. 사분위수(Q1=25%, Q3=75%)를 구한 후, IQR = Q3 - Q1을 계산합니다.
그리고 Q1 - 1.5×IQR보다 작거나 Q3 + 1.5×IQR보다 큰 값을 이상치로 간주합니다. 예를 들어, 데이터의 중간 50%가 40-60 사이라면(IQR=20), 10보다 작거나 90보다 큰 값이 이상치입니다.
이 방법은 평균과 표준편차를 사용하지 않아서 이상치 자체에 영향받지 않는다는 큰 장점이 있습니다. 박스플롯에서 수염 밖의 점들이 바로 이 기준으로 표시됩니다.
두 번째로, Z-score 방법은 데이터가 정규분포를 따른다고 가정합니다. Z-score = (값 - 평균) / 표준편차로 계산하며, 보통 |Z| > 3인 값을 이상치로 봅니다.
정규분포에서 99.7%의 데이터가 평균±3 표준편차 안에 있으므로, 그 밖의 값은 매우 드문 케이스입니다. 하지만 이 방법은 평균과 표준편차 자체가 이상치의 영향을 받기 때문에, 이상치가 많으면 제대로 작동하지 않을 수 있습니다.
데이터가 정규분포에 가까울 때 가장 효과적입니다. 세 번째로, Isolation Forest는 머신러닝 기반의 고급 방법입니다.
랜덤하게 데이터를 분할했을 때 빨리 고립되는 점을 이상치로 간주합니다. 이상치는 다른 점들과 떨어져 있어서 적은 분할로도 혼자 남게 되지만, 정상 데이터는 밀집되어 있어 많은 분할이 필요합니다.
이 방법의 큰 장점은 다차원 데이터에서도 잘 작동하고, 분포 가정이 필요 없다는 것입니다. contamination 매개변수로 예상 이상치 비율(예: 3%)을 지정할 수 있습니다.
여러분이 이 코드를 사용하면 데이터의 품질을 크게 개선할 수 있습니다. 이상치 제거 후 평균은 더 대표성 있는 값이 되고, 모델 학습도 정상 패턴에 집중할 수 있습니다.
실제로 이상치 처리만으로도 회귀 모델의 RMSE가 20-40% 개선되는 경우가 많습니다. 특히 선형 회귀는 이상치에 매우 민감하므로 처리가 필수적입니다.
이상치 처리 방법은 제거(완전 삭제), 캡핑(상한/하한으로 제한), 변환(로그 변환으로 극단값 압축), 대체(중앙값으로 교체) 등이 있습니다. 제거는 가장 간단하지만 데이터 손실이 있고, 캡핑은 정보는 보존하면서 극단성만 줄입니다.
도메인 지식에 따라 적절한 방법을 선택해야 하며, 진짜 극단값(예: 빌 게이츠의 재산)은 오류가 아니므로 별도로 분석하거나 모델링할 수 있습니다.
실전 팁
💡 이상치를 무조건 제거하지 말고 먼저 조사하세요. 데이터 입력 오류일 수도 있고, 중요한 극단 케이스일 수도 있습니다. 도메인 전문가와 상의하는 것이 좋습니다.
💡 시각화를 활용하세요. 박스플롯, 산점도, 히스토그램으로 이상치를 눈으로 확인하면 수치만으로는 놓칠 수 있는 패턴을 발견할 수 있습니다.
💡 다차원 이상치도 고려하세요. 키 180cm도 정상, 몸무게 70kg도 정상이지만, 180cm에 40kg는 이상할 수 있습니다. Isolation Forest나 Local Outlier Factor는 이런 경우에 효과적입니다.
💡 민감한 모델(선형 회귀, KNN)을 사용한다면 이상치 처리가 필수지만, 트리 기반 모델(Random Forest, XGBoost)은 이상치에 상대적으로 강합니다. 모델 선택도 중요한 전략입니다.
💡 로그 변환을 활용하세요. 오른쪽으로 긴 꼬리를 가진 분포(수입, 가격 등)는 로그 변환하면 극단값이 압축되고 정규분포에 가까워져 모델 학습이 쉬워집니다.
5. 피처 생성 - 새로운 의미 있는 변수 만들기
시작하며
여러분이 신용카드 사기 탐지 모델을 만든다고 해볼까요? 데이터에 '거래 금액'과 '거래 시간'이 있습니다.
그런데 사기꾼들은 보통 "짧은 시간에 여러 번 거래"하는 패턴을 보입니다. 원본 데이터만으로는 이 패턴을 찾기 어렵지만, "최근 1시간 내 거래 횟수"라는 새로운 변수를 만들면 사기를 훨씬 쉽게 탐지할 수 있습니다.
이처럼 원본 데이터를 조합하거나 변환하여 새로운 의미를 가진 변수를 만드는 것이 피처 엔지니어링의 핵심입니다. 머신러닝 모델은 여러분이 제공한 변수로만 학습하기 때문에, 좋은 피처를 만들면 단순한 모델로도 뛰어난 성능을 낼 수 있고, 나쁜 피처만 있으면 아무리 복잡한 모델을 써도 성능이 안 나옵니다.
실제로 "피처 엔지니어링이 모델 선택보다 10배 중요하다"는 말이 있을 정도입니다. 바로 이럴 때 필요한 것이 피처 생성 기법입니다.
도메인 지식과 창의성을 활용하여 원본 데이터에 숨겨진 패턴을 드러내는 새로운 변수를 만들면, 모델의 예측력이 극적으로 향상됩니다.
개요
간단히 말해서, 피처 생성은 기존 변수들을 조합, 변환, 집계하여 모델이 패턴을 더 쉽게 학습할 수 있는 새로운 변수를 만드는 과정입니다. 마치 요리에서 기본 재료(양파, 고기, 향신료)를 조합하여 새로운 맛(소스)을 만들어내는 것과 같습니다.
왜 피처 생성이 중요할까요? 첫째, 숨겨진 패턴을 명시적으로 드러냅니다.
예를 들어, '키'와 '몸무게'보다는 'BMI = 몸무게 / (키²)'가 건강 상태를 더 잘 나타냅니다. 둘째, 모델의 학습 부담을 줄입니다.
복잡한 비선형 관계를 모델이 스스로 찾게 하는 것보다, 여러분이 먼저 의미 있는 조합을 만들어주면 더 빠르고 정확하게 학습합니다. 셋째, 도메인 지식을 주입할 수 있습니다.
데이터 과학자가 업무 전문가의 인사이트를 변수로 표현하면, 모델이 실무적으로 의미 있는 예측을 합니다. 전통적으로는 단순한 사칙연산(합, 차, 곱, 비율)이나 다항식 변환을 사용했습니다.
하지만 이제는 시간 기반 피처(요일, 계절, 경과 시간), 집계 피처(그룹별 평균, 최대, 표준편차), 텍스트 피처(단어 개수, 감성 점수), 상호작용 피처(변수 간 곱셈) 등 다양한 고급 기법을 활용합니다. 피처 생성의 핵심 특징은 첫째, 도메인 지식이 매우 중요하다는 점입니다.
금융 전문가는 '부채비율', 의료 전문가는 'BMI' 같은 의미 있는 피처를 알고 있습니다. 둘째, 창의성이 필요합니다.
기존 변수를 새로운 시각으로 보는 능력이 중요합니다. 셋째, 반복적 실험이 필요합니다.
많은 피처를 만들어보고 모델 성능으로 검증해야 합니다. 이러한 특징들이 중요한 이유는 좋은 피처 하나가 모델 복잡도를 크게 줄이고 성능을 획기적으로 높일 수 있기 때문입니다.
코드 예제
import pandas as pd
import numpy as np
from datetime import datetime
# 샘플 데이터 - 전자상거래 거래 기록
df = pd.DataFrame({
'transaction_time': pd.to_datetime(['2024-01-15 14:30', '2024-01-15 22:15',
'2024-06-20 09:00', '2024-12-25 18:45']),
'amount': [50000, 200000, 30000, 150000],
'user_age': [25, 45, 30, 50],
'user_income': [3000, 6000, 4000, 8000], # 만원 단위
'product_category': ['의류', '전자제품', '식품', '전자제품']
})
# 1. 시간 기반 피처 생성 - 요일, 시간대, 계절 등
df['hour'] = df['transaction_time'].dt.hour
df['day_of_week'] = df['transaction_time'].dt.dayofweek # 0=월요일
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
df['is_night'] = ((df['hour'] >= 22) | (df['hour'] <= 6)).astype(int)
df['month'] = df['transaction_time'].dt.month
df['is_holiday_season'] = df['month'].isin([12, 1]).astype(int) # 연말연시
# 2. 수치 변수 조합 - 비율, 곱셈 등
df['income_to_age_ratio'] = df['user_income'] / df['user_age'] # 나이 대비 소득
df['amount_to_income_ratio'] = (df['amount'] / 10000) / df['user_income'] # 소득 대비 지출
df['is_high_value'] = (df['amount'] > 100000).astype(int) # 고액 거래 여부
# 3. 그룹별 집계 피처 - 카테고리별 평균 등
category_avg = df.groupby('product_category')['amount'].transform('mean')
df['amount_vs_category_avg'] = df['amount'] / category_avg # 카테고리 평균 대비
# 4. 다항식 피처 - 비선형 관계 포착
df['age_squared'] = df['user_age'] ** 2
df['log_amount'] = np.log1p(df['amount']) # log(1 + x)로 0 처리
print("원본 데이터:\n", df[['amount', 'user_age', 'user_income']].head())
print("\n생성된 피처:\n", df[['hour', 'is_weekend', 'is_night', 'income_to_age_ratio',
'amount_to_income_ratio', 'is_high_value']].head())
설명
이것이 하는 일: 위 코드는 전자상거래 거래 데이터에서 실무적으로 의미 있는 다양한 피처를 생성합니다. 원본 데이터 5개 컬럼에서 15개 이상의 새로운 인사이트를 담은 변수들을 만들어냅니다.
첫 번째로, 시간 기반 피처는 datetime 변수에서 숨겨진 패턴을 추출합니다. '14:30'이라는 시간에서 'hour=14', 'is_night=0', 'is_weekend=0' 같은 명시적인 정보를 만듭니다.
왜 중요할까요? 사람들의 구매 패턴은 시간대에 따라 다릅니다.
주말과 평일, 낮과 밤, 월초와 월말, 명절 시즌 등에 따라 구매 행동이 크게 달라집니다. 예를 들어, 심야 시간(22시 이후)의 고액 거래는 사기일 가능성이 높을 수 있습니다.
원본 datetime 값 그대로는 모델이 이런 패턴을 학습하기 어렵지만, 명시적인 플래그 변수로 만들면 쉽게 학습합니다. 두 번째로, 수치 변수 조합은 두 개 이상의 변수 간 관계를 하나의 변수로 표현합니다.
'amount_to_income_ratio'는 단순히 거래 금액이 아니라 "이 사람의 소득 대비 얼마나 큰 지출인가"를 나타냅니다. 같은 100만원이라도 월소득 300만원인 사람에게는 큰 지출(비율=33%)이지만, 월소득 1000만원인 사람에게는 작은 지출(비율=10%)입니다.
이런 상대적 개념을 모델에게 알려주면, 절대값만 볼 때보다 훨씬 정확한 예측이 가능합니다. 특히 사기 탐지에서는 "소득 대비 비정상적으로 큰 지출"이 중요한 신호입니다.
세 번째로, 그룹별 집계 피처는 각 데이터 포인트를 그룹 전체와 비교합니다. 'amount_vs_category_avg'는 "이 거래가 같은 카테고리의 평균 거래보다 얼마나 큰가"를 알려줍니다.
전자제품 카테고리의 평균이 175,000원이라면, 200,000원 거래는 평균보다 약간 높고(비율=1.14), 150,000원은 약간 낮습니다(비율=0.86). 이렇게 하면 카테고리별로 가격대가 다른 것을 정규화할 수 있어, 모델이 더 공정하게 비교할 수 있습니다.
네 번째로, 다항식 피처와 로그 변환은 비선형 관계를 선형 모델에서도 학습 가능하게 만듭니다. 나이와 소득의 관계가 선형이 아니라 나이²에 비례한다면, age_squared를 추가하면 선형 회귀로도 이 패턴을 잡을 수 있습니다.
log_amount는 큰 금액의 차이를 압축하여(1만원→10만원 변화와 100만원→1000만원 변화를 비슷하게), 왜도가 큰 분포를 정규분포에 가깝게 만듭니다. 여러분이 이 코드를 사용하면 모델 성능이 극적으로 향상될 수 있습니다.
실제로 Kaggle 대회에서 상위권 솔루션의 80%는 모델보다 피처 엔지니어링에서 우위를 점했다고 합니다. 좋은 피처 10개가 나쁜 피처 100개보다 낫고, 단순한 로지스틱 회귀에 좋은 피처를 주면 복잡한 딥러닝보다 나을 수 있습니다.
또한 피처 생성은 모델 해석력도 높입니다. "소득 대비 지출 비율이 높아서 사기로 판단했다"는 설명은 실무에서 이해하고 행동하기 쉽습니다.
실전 팁
💡 도메인 전문가와 협업하세요. 데이터 과학 기술만으로는 한계가 있고, 업무 지식이 있는 사람이 "이런 조합이 의미 있다"고 알려주면 골드 피처를 찾을 수 있습니다.
💡 피처 중요도를 분석하여 쓸모없는 피처는 제거하세요. 너무 많은 피처는 과적합과 계산 비용 증가를 유발합니다. Random Forest나 XGBoost의 feature_importances_를 활용하세요.
💡 리키지(leakage)를 조심하세요. 미래 정보를 사용하는 피처(예: 대출 연체 예측에서 '연체 후 조치 여부' 사용)는 테스트에서는 사용할 수 없어 실제 성능이 크게 떨어집니다.
💡 자동화 도구를 활용하되 맹신하지 마세요. Featuretools 같은 라이브러리가 수백 개의 피처를 자동 생성하지만, 도메인 지식 기반의 수작업 피처가 더 효과적일 때가 많습니다.
💡 시간 관련 피처는 거의 항상 유용합니다. 날짜에서 요일, 주말, 월, 분기, 공휴일, 계절, 이벤트 기간 등을 추출하면 시계열 데이터의 성능이 크게 향상됩니다.
6. 차원 축소 - 많은 변수를 적은 변수로 압축하기
시작하며
여러분이 고객 설문조사 데이터를 분석하는데 100개의 질문이 있다고 상상해보세요. 그런데 자세히 보니 많은 질문들이 비슷한 내용을 묻고 있습니다.
"제품에 만족하나요?", "다시 구매하시겠어요?", "친구에게 추천하시겠어요?" 같은 질문들은 모두 '고객 만족도'라는 하나의 개념을 다르게 표현한 것입니다. 100개 변수를 전부 사용하는 것보다, 핵심 개념 10개로 압축하면 더 명확하고 효율적입니다.
이런 문제는 실무에서 정말 흔합니다. 센서 데이터는 수백 개의 신호를 기록하지만 대부분 서로 연관되어 있고, 이미지는 수천 개의 픽셀을 가지지만 실제 의미 있는 패턴은 훨씬 적으며, 텍스트 데이터는 수만 개의 단어를 포함하지만 핵심 주제는 몇 개뿐입니다.
너무 많은 변수는 계산 비용을 증가시키고, 과적합을 유발하며, 해석을 어렵게 만들고, 차원의 저주(데이터가 희소해지는 문제)를 일으킵니다. 바로 이럴 때 필요한 것이 차원 축소 기법입니다.
원본 데이터의 중요한 정보는 최대한 보존하면서, 변수 개수를 크게 줄이면 모델이 빠르고 정확하게 학습할 수 있고, 시각화와 해석도 쉬워집니다.
개요
간단히 말해서, 차원 축소는 많은 변수를 적은 수의 핵심 변수로 압축하는 과정입니다. 마치 긴 소설을 핵심 줄거리만 남긴 요약본으로 만드는 것처럼, 정보 손실을 최소화하면서 복잡도를 줄이는 것입니다.
왜 차원 축소가 필요할까요? 첫째, 계산 효율성을 높입니다.
1000개 변수 대신 10개 변수를 사용하면 학습 속도가 수십 배 빨라집니다. 둘째, 과적합을 방지합니다.
변수가 너무 많으면 모델이 노이즈까지 학습하는데, 핵심 변수만 사용하면 일반화 성능이 향상됩니다. 예를 들어, 100개 변수로 학습한 모델이 훈련 정확도 99%, 테스트 정확도 70%라면 과적합인데, 10개 핵심 변수로 줄이면 테스트 정확도가 85%로 오를 수 있습니다.
셋째, 시각화가 가능해집니다. 100차원 데이터는 시각화가 불가능하지만, 2-3차원으로 줄이면 산점도로 패턴을 직관적으로 볼 수 있습니다.
전통적으로는 단순히 상관관계가 높은 변수들을 수동으로 제거했습니다. 하지만 이제는 PCA(주성분 분석), t-SNE, UMAP 같은 정교한 알고리즘을 사용합니다.
PCA는 변수들의 선형 조합으로 분산을 최대화하는 새로운 축을 찾고, t-SNE는 고차원에서 가까운 점들이 저차원에서도 가깝도록 비선형 매핑을 수행합니다. 차원 축소의 핵심 특징은 첫째, 원본 변수를 새로운 변수로 변환한다는 점입니다(변수 선택이 아닌 변수 생성).
둘째, 정보 손실과 복잡도 감소 사이의 트레이드오프가 있습니다. 셋째, 선형 방법(PCA)과 비선형 방법(t-SNE)이 다른 용도에 적합합니다.
이러한 특징들이 중요한 이유는 데이터의 본질적인 구조를 유지하면서도 불필요한 복잡성을 제거할 수 있기 때문입니다.
코드 예제
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
import numpy as np
import pandas as pd
# 샘플 데이터 - 상관관계가 높은 변수들
np.random.seed(42)
n_samples = 100
# 키와 몸무게는 상관관계가 높음
height = np.random.normal(170, 10, n_samples)
weight = height * 0.7 + np.random.normal(0, 5, n_samples) # 키에 비례하지만 노이즈 포함
# 나이와 경력도 상관관계가 높음
age = np.random.normal(35, 10, n_samples)
experience = (age - 22) * 0.8 + np.random.normal(0, 2, n_samples) # 나이-22와 비례
df = pd.DataFrame({
'height': height,
'weight': weight,
'age': age,
'experience': experience
})
# 1단계: 스케일링 (PCA는 스케일에 민감함)
scaler = StandardScaler()
df_scaled = scaler.fit_transform(df)
# 2단계: PCA 적용 - 4개 변수를 2개 주성분으로 압축
pca = PCA(n_components=2)
principal_components = pca.fit_transform(df_scaled)
# 결과 분석
df_pca = pd.DataFrame(
principal_components,
columns=['PC1', 'PC2']
)
print("원본 데이터 shape:", df.shape)
print("PCA 후 데이터 shape:", df_pca.shape)
print("\n각 주성분이 설명하는 분산 비율:", pca.explained_variance_ratio_)
print("누적 설명 분산:", np.cumsum(pca.explained_variance_ratio_))
print("\n주성분 1의 원본 변수 기여도:\n",
pd.DataFrame(pca.components_[0], index=df.columns, columns=['PC1']))
설명
이것이 하는 일: 위 코드는 서로 상관관계가 높은 4개 변수(키, 몸무게, 나이, 경력)를 2개의 독립적인 주성분으로 압축합니다. 키와 몸무게는 서로 연관되어 있고, 나이와 경력도 연관되어 있어서, 실제로는 2개의 독립적인 정보만 있는 셈입니다.
첫 번째로, 스케일링이 필수적입니다. PCA는 분산이 큰 변수에 더 큰 가중치를 주기 때문에, 키(170cm)와 몸무게(70kg)가 스케일이 다르면 왜곡된 결과가 나옵니다.
StandardScaler로 모든 변수를 평균 0, 분산 1로 만들면 공정한 비교가 가능합니다. 이것은 차원 축소 전처리의 거의 필수 단계입니다.
두 번째로, PCA는 새로운 축(주성분)을 찾습니다. 첫 번째 주성분(PC1)은 데이터의 분산을 가장 많이 설명하는 방향입니다.
예를 들어, 키와 몸무게가 함께 증가하는 패턴이 있다면, PC1은 "전반적인 체격"을 나타내는 축이 됩니다. 두 번째 주성분(PC2)은 PC1과 직교(perpendicular)하면서 남은 분산을 최대화하는 방향으로, "키에 비해 몸무게가 많이 나가는지" 같은 정보를 담을 수 있습니다.
이렇게 원본 4개 변수의 정보를 2개의 새로운 변수로 요약합니다. 세 번째로, explained_variance_ratio_는 각 주성분이 원본 데이터의 분산 중 몇 %를 설명하는지 알려줍니다.
예를 들어, PC1이 70%, PC2가 20%라면, 2개 주성분으로 원본 데이터의 90% 정보를 보존했다는 뜻입니다. 이것으로 "몇 개의 주성분을 사용할지" 결정할 수 있습니다.
보통 누적 설명 분산이 85-95%가 되도록 주성분 개수를 선택합니다. 원본 100개 변수에서 10개 주성분이 90% 정보를 담는다면, 나머지 90개 변수는 노이즈에 가까운 것입니다.
네 번째로, components_ 속성은 각 주성분이 원본 변수들의 어떤 조합인지 보여줍니다. 예를 들어, PC1 = 0.5×키 + 0.5×몸무게 + 0.5×나이 + 0.5×경력이라면, PC1은 "전반적인 크기/성숙도"를 나타냅니다.
이렇게 주성분의 의미를 해석할 수 있어, 단순한 블랙박스 변환이 아니라 도메인 지식과 연결할 수 있습니다. 여러분이 이 코드를 사용하면 변수가 수십, 수백 개인 복잡한 데이터를 몇 개의 핵심 요소로 정리할 수 있습니다.
실제로 이미지 인식에서는 수천 차원의 픽셀 데이터를 수십 차원으로 압축하여 학습 속도를 100배 높이면서도 성능은 5% 이하로만 감소합니다. 금융 데이터에서는 수백 개 지표를 "성장성", "안정성", "수익성" 같은 몇 개 팩터로 요약하여 해석력을 크게 높입니다.
실무에서는 PCA 외에도 용도에 따라 다른 기법을 사용합니다. t-SNE나 UMAP은 시각화에 최적화되어 있어 2-3차원으로 줄여서 산점도를 그릴 때 좋고, Autoencoder는 딥러닝으로 비선형 압축을 수행하며, TruncatedSVD는 희소 행렬(텍스트 데이터)에 효율적입니다.
각 방법의 특성을 이해하고 데이터와 목적에 맞게 선택하는 것이 중요합니다.
실전 팁
💡 PCA 전에 반드시 스케일링하세요. 스케일이 다른 변수들(예: 키 cm와 몸무게 kg)을 그대로 PCA하면 큰 값이 주성분을 지배합니다.
💡 누적 설명 분산을 90-95%로 유지하는 주성분 개수를 선택하세요. scree plot을 그려서 기울기가 급격히 줄어드는 지점(elbow)을 찾는 것도 좋은 방법입니다.
💡 PCA는 선형 관계만 잡습니다. 비선형 패턴이 중요하다면 Kernel PCA나 Autoencoder를 사용하세요. 예를 들어, 원형 패턴은 PCA로 잘 압축되지 않습니다.
💡 시각화가 목적이라면 t-SNE나 UMAP이 PCA보다 낫습니다. PCA는 전역 구조를 보존하지만, t-SNE는 국소적인 군집을 더 명확히 보여줍니다.
💡 PCA는 가역적입니다. inverse_transform으로 주성분을 원본 공간으로 되돌릴 수 있어, 노이즈 제거나 이상치 탐지에도 활용할 수 있습니다.
7. 텍스트 데이터 전처리 - 문자를 숫자로 변환하기
시작하며
여러분이 고객 리뷰 데이터로 감성 분석을 한다고 해볼까요? "이 제품 정말 좋아요!" 같은 텍스트를 머신러닝 모델에 넣고 싶은데, 모델은 숫자만 이해할 수 있습니다.
단순히 글자를 숫자로 바꾸는 것이 아니라, 텍스트의 의미를 숫자로 표현해야 합니다. 또한 "좋아요"와 "좋아요!"는 같은 의미인데, 특수문자 때문에 다르게 인식되면 안 되겠죠.
텍스트 데이터는 매우 복잡합니다. 대소문자, 특수문자, 불필요한 공백, 조사(은/는, 이/가), 의미 없는 단어(a, the), 띄어쓰기 오류 등 처리해야 할 것이 산더미입니다.
또한 "좋다", "좋은", "좋았다"는 모두 같은 어근을 가지지만 형태가 다르고, "apple"과 "apples"도 단수/복수 차이일 뿐 같은 개념입니다. 이런 것들을 정리하지 않으면 모델이 같은 의미의 단어를 다른 것으로 학습하게 됩니다.
바로 이럴 때 필요한 것이 텍스트 전처리 기법입니다. 텍스트를 정제하고, 토큰화하고, 정규화하고, 벡터화하면 모델이 텍스트의 의미를 제대로 학습할 수 있습니다.
개요
간단히 말해서, 텍스트 전처리는 원시 텍스트를 머신러닝 모델이 이해할 수 있는 숫자 형태로 변환하는 일련의 과정입니다. 마치 외국어 문서를 번역하고 정리하여 이해 가능한 형태로 만드는 것과 같습니다.
왜 텍스트 전처리가 중요할까요? 첫째, 노이즈를 제거합니다.
URL, 이메일, 특수문자, HTML 태그 같은 것들은 대부분 분석에 불필요하며, 오히려 모델을 혼란스럽게 만듭니다. 예를 들어, "좋아요!!!"와 "좋아요"는 같은 의미인데, 느낌표 개수 때문에 다르게 처리되면 안 됩니다.
둘째, 차원을 줄입니다. "run", "running", "runs", "ran"을 모두 "run"으로 통일하면 변수 개수가 75% 줄어들고, 모델이 학습할 샘플도 4배 증가하는 효과가 있습니다.
셋째, 의미를 보존합니다. 단순히 단어를 숫자로 바꾸는 것이 아니라, "좋다"와 "훌륭하다"가 비슷한 의미임을 모델이 알 수 있도록 임베딩을 사용합니다.
전통적으로는 Bag-of-Words나 TF-IDF로 단어 빈도를 세는 단순한 방법을 사용했습니다. 하지만 이제는 Word2Vec, GloVe, BERT 같은 임베딩 기법으로 단어의 의미와 문맥을 벡터로 표현합니다.
이렇게 하면 "왕 - 남자 + 여자 = 여왕" 같은 의미 연산도 가능해집니다. 텍스트 전처리의 핵심 특징은 첫째, 단계적 파이프라인으로 진행된다는 점입니다(정제 → 토큰화 → 정규화 → 벡터화).
둘째, 언어와 도메인에 따라 방법이 달라집니다(한국어는 형태소 분석, 영어는 어간 추출). 셋째, 정보 손실과 노이즈 제거 사이의 균형이 중요합니다.
이러한 특징들이 중요한 이유는 텍스트의 본질적인 의미는 보존하면서도 모델이 학습하기 좋은 형태로 변환해야 하기 때문입니다.
코드 예제
import re
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
import numpy as np
# 샘플 텍스트 데이터 - 실제 리뷰 형식
reviews = [
"이 제품 정말 좋아요!!! 강력 추천합니다 ^^",
"별로예요... 기대 이하였습니다.",
"가격 대비 훌륭합니다. 좋아요!",
"최악이에요 ㅠㅠ 환불하고 싶네요"
]
# 1단계: 텍스트 정제 (클리닝)
def clean_text(text):
# 특수문자, 이모티콘 제거 (한글, 영문, 숫자, 공백만 남김)
text = re.sub(r'[^가-힣a-zA-Z0-9\s]', '', text)
# 연속된 공백을 하나로
text = re.sub(r'\s+', ' ', text)
# 소문자 변환 (영어의 경우)
text = text.lower().strip()
return text
cleaned_reviews = [clean_text(review) for review in reviews]
# 2단계: 불용어 제거 및 토큰화는 Vectorizer가 자동으로 처리
# 한국어 불용어 리스트 (예시)
korean_stopwords = ['이', '그', '저', '것', '수', '등', '들']
# 3단계: 벡터화 방법 1 - CountVectorizer (단어 빈도)
count_vectorizer = CountVectorizer(
max_features=50, # 최대 50개 단어만 사용
min_df=1, # 최소 1개 문서에 등장
stop_words=korean_stopwords
)
count_matrix = count_vectorizer.fit_transform(cleaned_reviews)
# 3단계: 벡터화 방법 2 - TF-IDF (중요도 가중치)
tfidf_vectorizer = TfidfVectorizer(
max_features=50,
min_df=1,
stop_words=korean_stopwords
)
tfidf_matrix = tfidf_vectorizer.fit_transform(cleaned_reviews)
print("원본 텍스트:", reviews[0])
print("정제된 텍스트:", cleaned_reviews[0])
print("\n추출된 단어(vocabulary):", count_vectorizer.get_feature_names_out())
print("\nCount Matrix shape:", count_matrix.shape)
print("TF-IDF Matrix shape:", tfidf_matrix.shape)
print("\n첫 번째 리뷰의 TF-IDF 벡터:\n",
pd.DataFrame(tfidf_matrix[0].toarray(),
columns=tfidf_vectorizer.get_feature_names_out()))
설명
이것이 하는 일: 위 코드는 실제 고객 리뷰 같은 지저분한 텍스트("좋아요!!!", "ㅠㅠ" 같은 표현 포함)를 머신러닝 모델에 입력할 수 있는 깔끔한 숫자 벡터로 변환합니다. "이 제품 정말 좋아요!!!
강력 추천합니다 ^^"가 [0.42, 0, 0.58, ...] 같은 숫자 배열이 됩니다. 첫 번째로, 텍스트 정제는 노이즈를 제거합니다.
정규표현식(regex)을 사용하여 특수문자(!!! ^^), 이모티콘(ㅠㅠ), 불필요한 공백을 제거합니다.
예를 들어, "좋아요!!!"는 "좋아요"로, "최악이에요 ㅠㅠ"는 "최악이에요"로 정리됩니다. 이렇게 하면 같은 의미의 단어가 다른 형태로 표현되는 것을 방지할 수 있습니다.
re.sub(r'[^가-힣a-zA-Z0-9\s]', '', text)는 "한글, 영문, 숫자, 공백이 아닌 모든 것을 제거하라"는 뜻입니다. 두 번째로, CountVectorizer는 가장 기본적인 벡터화 방법입니다.
각 문서에서 각 단어가 몇 번 등장하는지 세어서 벡터로 만듭니다. 예를 들어, 전체 어휘가 ["제품", "좋아요", "추천", "별로"] 4개라면, "제품 좋아요 좋아요"는 [1, 2, 0, 0]으로 표현됩니다.
max_features=50은 빈도가 높은 상위 50개 단어만 사용하라는 뜻으로, 차원을 줄이고 중요한 단어에 집중합니다. min_df=1은 최소 1개 문서에 등장해야 한다는 뜻으로, 너무 희귀한 단어를 제외합니다.
세 번째로, TF-IDF는 더 정교한 방법입니다. 단순 빈도가 아니라 "이 단어가 이 문서에서 얼마나 중요한가"를 측정합니다.
TF(Term Frequency)는 문서 내 빈도이고, IDF(Inverse Document Frequency)는 전체 문서에서의 희소성입니다. 예를 들어, "좋아요"가 한 리뷰에 5번 나오면 TF가 높지만, 모든 리뷰에 등장한다면 IDF가 낮아서 최종 점수가 중간 정도가 됩니다.
반면 "환불"이 한 리뷰에만 나온다면 IDF가 높아서 그 리뷰의 특징을 잘 나타내는 단어가 됩니다. 공식은 TF-IDF = TF × log(전체 문서 수 / 해당 단어가 등장한 문서 수)입니다.
네 번째로, 결과 행렬은 (문서 수 × 어휘 크기) 형태입니다. 4개 리뷰에서 10개 단어가 추출되었다면 (4, 10) 행렬이 됩니다.
각 행은 하나의 리뷰를 나타내고, 각 열은 하나의 단어를 나타냅니다. 이 행렬을 머신러닝 모델에 입력하면, 모델은 "어떤 단어가 긍정/부정과 연관되는지" 학습할 수 있습니다.
예를 들어, "좋아요", "추천"이 많은 리뷰는 긍정, "최악", "환불"이 많은 리뷰는 부정으로 분류하게 됩니다. 여러분이 이 코드를 사용하면 텍스트 데이터로 감성 분석, 스팸 탐지, 문서 분류, 추천 시스템 등 다양한 애플리케이션을 만들 수 있습니다.
실제로 전자상거래 플랫폼은 수백만 개의 리뷰를 TF-IDF로 벡터화하여 자동으로 별점을 예측하거나, 가짜 리뷰를 탐지하거나, 제품 개선 포인트를 찾아냅니다. 뉴스 사이트는 기사를 벡터화하여 비슷한 기사를 추천하고, 은행은 고객 문의를 자동으로 카테고리별로 분류합니다.
실전 팁
💡 한국어는 형태소 분석기(KoNLPy, Mecab)를 사용하세요. "좋았습니다"를 "좋다 + 과거 + 합니다"로 분리하면 "좋다"라는 어근을 추출할 수 있어 정확도가 크게 향상됩니다.
💡 도메인 특화 불용어를 추가하세요. 일반 불용어("이", "그") 외에도 여러분의 데이터에서 의미 없이 자주 등장하는 단어(예: 리뷰 데이터의 "제품", "구매")를 제거하면 더 깔끔합니다.
💡 n-gram을 사용하여 문맥을 포착하세요. "not good"은 부정인데, 단어별로 보면 "good"이 긍정으로 오인될 수 있습니다. ngram_range=(1,2)로 설정하면 "not good"을 하나의 단어로 취급합니다.
💡 최소/최대 문서 빈도를 설정하세요. min_df=5는 5개 미만 문서에 등장하는 희귀 단어 제거, max_df=0.8은 80% 이상 문서에 등장하는 너무 흔한 단어 제거로, 노이즈를 줄입니다.
💡 딥러닝을 사용한다면 Word2Vec, GloVe, BERT 임베딩을 고려하세요. TF-IDF는 단어의 의미를 모르지만, 임베딩은 "king - man + woman = queen" 같은 의미 연산이 가능합니다.
8. 시계열 데이터 피처 생성 - 시간의 흐름을 변수로 만들기
시작하며
여러분이 주식 가격 예측 모델을 만든다고 해볼까요? 오늘의 가격만 보고 내일을 예측하기는 어렵습니다.
하지만 "최근 7일 평균 가격", "가격이 상승 추세인지 하락 추세인지", "어제 대비 변화율" 같은 정보가 있다면 훨씬 정확한 예측이 가능합니다. 시간의 흐름 속에 숨겨진 패턴을 변수로 만드는 것이 핵심입니다.
시계열 데이터는 독특한 특징이 있습니다. 과거가 미래에 영향을 미치고(자기상관), 계절성 패턴이 반복되며(여름마다 에어컨 판매 증가), 트렌드가 존재합니다(스마트폰 보급률 지속 증가).
일반적인 피처 엔지니어링 기법만으로는 이런 시간적 특성을 제대로 포착할 수 없습니다. 또한 미래 정보를 사용하면 안 되므로(데이터 누수), 과거 정보만으로 피처를 만들어야 합니다.
바로 이럴 때 필요한 것이 시계열 피처 생성 기법입니다. 지연(lag), 이동 평균, 차분, 롤링 통계 같은 시간 기반 피처를 만들면 과거 패턴을 효과적으로 학습하여 미래를 예측할 수 있습니다.
개요
간단히 말해서, 시계열 피처 생성은 과거 시점의 값이나 과거 기간의 통계량을 새로운 변수로 만드는 과정입니다. 마치 날씨 예측에서 "어제 온도", "최근 일주일 평균 온도", "작년 같은 날 온도"를 참고하는 것과 같습니다.
왜 시계열 피처가 중요할까요? 첫째, 과거 패턴을 명시적으로 모델에 알려줍니다.
머신러닝 모델은 기본적으로 시간 순서를 모르므로, "7일 전 값"을 직접 변수로 만들어줘야 합니다. 둘째, 계절성과 트렌드를 포착합니다.
"같은 요일 평균", "전월 대비 증가율" 같은 피처로 주기적 패턴과 장기 추세를 학습합니다. 예를 들어, 소매 판매 데이터에서 "작년 12월 판매량"을 피처로 추가하면 연말 쇼핑 시즌의 계절성을 학습할 수 있습니다.
셋째, 노이즈를 줄입니다. 이동 평균을 사용하면 일시적 변동을 평탄화하여 진짜 추세에 집중할 수 있습니다.
전통적으로는 단순히 이전 시점 값(lag)을 사용했습니다. 하지만 이제는 다양한 윈도우의 이동 평균, 지수 이동 평균(EMA), 변화율, 가속도(변화율의 변화), 롤링 표준편차, 푸리에 변환 피처 등 정교한 기법을 사용합니다.
딥러닝에서는 LSTM이 자동으로 시간 패턴을 학습하지만, 전통적인 모델(XGBoost, Random Forest)에서는 명시적인 시계열 피처가 필수입니다. 시계열 피처의 핵심 특징은 첫째, 과거 정보만 사용해야 한다는 점입니다(미래 정보 누수 방지).
둘째, 윈도우 크기가 중요합니다(1일, 7일, 30일 등). 셋째, 데이터 빈도에 맞춰야 합니다(일별, 주별, 월별).
이러한 특징들이 중요한 이유는 시간의 흐름을 올바르게 모델링해야 실제 예측 시나리오에서도 작동하기 때문입니다.
코드 예제
import pandas as pd
import numpy as np
# 샘플 시계열 데이터 - 일별 판매량
dates = pd.date_range('2024-01-01', periods=30, freq='D')
np.random.seed(42)
# 트렌드 + 주간 계절성 + 노이즈
trend = np.arange(30) * 2
seasonality = 10 * np.sin(np.arange(30) * 2 * np.pi / 7) # 주간 주기
noise = np.random.normal(0, 5, 30)
sales = 100 + trend + seasonality + noise
df = pd.DataFrame({'date': dates, 'sales': sales})
df.set_index('date', inplace=True)
# 1. Lag 피처 - 과거 시점의 값
df['sales_lag_1'] = df['sales'].shift(1) # 1일 전
df['sales_lag_7'] = df['sales'].shift(7) # 7일 전 (같은 요일)
# 2. 이동 평균 (롤링 윈도우) - 최근 기간 평균
df['sales_ma_7'] = df['sales'].rolling(window=7).mean() # 최근 7일 평균
df['sales_ma_14'] = df['sales'].rolling(window=14).mean() # 최근 14일 평균
# 3. 차분 - 변화량
df['sales_diff_1'] = df['sales'].diff(1) # 전일 대비 변화
df['sales_pct_change'] = df['sales'].pct_change(1) * 100 # 전일 대비 변화율(%)
# 4. 롤링 통계 - 변동성 측정
df['sales_rolling_std'] = df['sales'].rolling(window=7).std() # 최근 7일 표준편차
df['sales_rolling_min'] = df['sales'].rolling(window=7).min() # 최근 7일 최소값
df['sales_rolling_max'] = df['sales'].rolling(window=7).max() # 최근 7일 최대값
# 5. 확장 윈도우 - 처음부터 현재까지 통계
df['sales_expanding_mean'] = df['sales'].expanding().mean() # 누적 평균
# 6. 시간 피처 - 날짜에서 추출
df['day_of_week'] = df.index.dayofweek # 요일 (0=월요일)
df['day_of_month'] = df.index.day # 일
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int) # 주말 여부
print("원본 데이터:\n", df[['sales']].head(10))
print("\n시계열 피처:\n", df[['sales', 'sales_lag_1', 'sales_ma_7', 'sales_diff_1',
'sales_pct_change']].head(10))
설명
이것이 하는 일: 위 코드는 일별 판매량 시계열 데이터에서 과거 패턴을 포착하는 다양한 피처를 생성합니다. 단순히 "오늘 판매량"만 보는 것이 아니라, "어제 판매량", "최근 일주일 평균", "전일 대비 증가율" 같은 시간적 맥락을 변수로 만듭니다.
첫 번째로, Lag 피처는 과거 특정 시점의 값을 가져옵니다. shift(1)은 모든 값을 한 칸씩 아래로 밀어서, 1월 2일 행에 1월 1일 값이 들어가게 합니다.
이렇게 하면 모델이 "어제 판매량이 100개였을 때 오늘은 보통 몇 개 팔릴까?"를 학습할 수 있습니다. sales_lag_7은 7일 전 값으로, 같은 요일의 패턴을 포착합니다.
예를 들어, 매주 토요일에 판매가 급증한다면, lag_7이 강력한 예측 변수가 됩니다. 주의할 점은 첫 번째 행은 이전 값이 없어서 NaN이 됩니다.
두 번째로, 이동 평균(Moving Average)은 최근 N일의 평균을 계산합니다. rolling(window=7).mean()은 현재 시점을 포함한 최근 7일 평균입니다.
이것은 일시적인 노이즈를 평탄화하여 진짜 추세를 드러냅니다. 예를 들어, 어느 날 갑자기 판매가 급증했다면 단발성 이벤트인지 추세 변화의 시작인지 판단하기 어렵지만, 7일 평균이 지속적으로 상승한다면 진짜 추세 변화일 가능성이 높습니다.
7일과 14일 이동 평균을 함께 사용하면 단기/장기 추세를 비교할 수 있습니다(골든 크로스/데드 크로스 전략). 세 번째로, 차분(Differencing)은 현재 값에서 이전 값을 빼서 변화량을 계산합니다.
diff(1)은 "오늘이 어제보다 얼마나 증가했는가"를 나타냅니다. 이것은 트렌드를 제거하여 정상성(stationarity)을 만드는 데 유용합니다.
예를 들어, 판매량이 매일 5개씩 증가하는 추세라면, 원본 값은 100, 105, 110...으로 계속 커지지만, 차분하면 5, 5, 5...로 안정적입니다. pct_change()는 백분율 변화로, "10% 증가"처럼 상대적 변화를 나타내어 스케일이 다른 시계열을 비교할 때 유용합니다.
네 번째로, 롤링 통계는 변동성과 극단값을 포착합니다. rolling(7).std()는 최근 7일 표준편차로, 판매가 얼마나 불규칙한지 측정합니다.
표준편차가 높으면 예측 불확실성이 크고, 낮으면 안정적입니다. rolling_max - rolling_min은 최근 7일 변동 폭으로, 이것이 크면 변동성 높은 기간입니다.
이런 피처는 이상 탐지에도 유용합니다. 예를 들어, "최근 7일 최대값보다 50% 높은 판매"가 발생하면 특별한 이벤트나 오류일 수 있습니다.
다섯 번째로, expanding 윈도우는 처음부터 현재까지 누적 통계를 계산합니다. expanding().mean()은 1일차는 1일 평균, 10일차는 1-10일 평균, 30일차는 1-30일 평균입니다.
이것은 "전체 기간 대비 현재 성과"를 비교할 때 유용합니다. 예를 들어, 현재 판매량이 누적 평균보다 높으면 최근 성과가 좋다는 뜻입니다.
여러분이 이 코드를 사용하면 주식 가격, 날씨, 에너지 소비, 트래픽, 판매량 등 모든 시계열 데이터를 효과적으로 예측할 수 있습니다. 실제로 시계열 피처를 추가하면 모델 RMSE가 30-50% 개선되는 경우가 많습니다.
특히 XGBoost나 Random Forest 같은 트리 기반 모델은 시간 정보를 자동으로 학습하지 못하므로, 명시적인 시계열 피처가 필수입니다.
실전 팁
💡 미래 정보 누수를 절대 방지하세요. rolling().mean()은 기본적으로 현재 포함 과거 값만 사용하지만, 미래 값이 섞이면 테스트 성능이 폭락합니다. shift()로 한 칸 미뤄서 안전하게 만드세요.
💡 여러 윈도우 크기를 실험하세요. 데이터에 따라 최적 윈도우가 다릅니다. 주간 패턴이 있으면 7일, 월간 패턴이 있으면 30일을 시도해보고 교차 검증으로 최선을 선택하세요.
💡 계절성 lag를 추가하세요. 주간 데이터는 lag_52(작년 같은 주), 월간 데이터는 lag_12(작년 같은 달)를 추가하면 연간 계절성을 포착할 수 있습니다.
💡 차분으로 정상성을 만드세요. 많은 시계열 모델(ARIMA)은 정상성을 가정합니다. diff()로 트렌드를 제거하고, 필요하면 2차 차분(diff().diff())도 시도하세요
댓글 (0)
함께 보면 좋은 카드 뉴스
데이터 증강과 정규화 완벽 가이드
머신러닝 모델의 성능을 극대화하는 핵심 기법인 데이터 증강과 정규화에 대해 알아봅니다. 실무에서 바로 활용할 수 있는 다양한 기법과 실전 예제를 통해 과적합을 방지하고 모델 성능을 향상시키는 방법을 배웁니다.
ResNet과 Skip Connection 완벽 가이드
딥러닝 모델이 깊어질수록 성능이 떨어지는 문제를 해결한 혁신적인 기법, ResNet과 Skip Connection을 초급자도 이해할 수 있도록 쉽게 설명합니다. 실제 구현 코드와 함께 배워보세요.
CNN 아키텍처 완벽 가이드 LeNet AlexNet VGGNet
컴퓨터 비전의 기초가 되는 세 가지 핵심 CNN 아키텍처를 배웁니다. 손글씨 인식부터 이미지 분류까지, 딥러닝의 발전 과정을 따라가며 각 모델의 구조와 특징을 실습 코드와 함께 이해합니다.
CNN 기초 Convolution과 Pooling 완벽 가이드
CNN의 핵심인 Convolution과 Pooling을 초급자도 쉽게 이해할 수 있도록 설명합니다. 이미지 인식의 원리부터 실제 코드 구현까지, 실무에서 바로 활용 가능한 내용을 담았습니다.
TensorFlow와 Keras 완벽 입문 가이드
머신러닝과 딥러닝의 세계로 들어가는 첫걸음! TensorFlow와 Keras 프레임워크를 처음 접하는 분들을 위한 친절한 가이드입니다. 실무에서 바로 활용할 수 있는 핵심 개념과 예제를 통해 AI 모델 개발의 기초를 탄탄히 다져보세요.