본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 3. · 12 Views
정상성 검정 및 변환 완벽 가이드
시계열 데이터 분석의 핵심인 정상성 개념부터 ADF 검정, 차분, 로그 변환까지 초급 개발자도 이해할 수 있도록 실무 예제와 함께 설명합니다.
목차
- 정상성이란_무엇인가
- ADF_검정으로_정상성_확인하기
- 차분으로_정상성_확보하기
- 로그_변환으로_분산_안정화하기
- Box-Cox_변환
- 계절성_제거를_위한_계절_차분
- KPSS_검정으로_이중_확인하기
- 실전_파이프라인_구축하기
1. 정상성이란 무엇인가
어느 날 김개발 씨는 주식 가격 예측 모델을 만들다가 이상한 결과를 마주했습니다. 분명히 좋은 알고리즘을 사용했는데, 예측 결과가 엉망이었습니다.
선배 박시니어 씨가 다가와 한마디 했습니다. "혹시 정상성 검정 해봤어요?"
**정상성(Stationarity)**이란 시계열 데이터의 통계적 특성이 시간에 따라 변하지 않는 성질을 말합니다. 마치 잔잔한 호수처럼 평균과 분산이 일정하게 유지되는 상태입니다.
대부분의 시계열 예측 모델은 이 정상성을 전제로 하기 때문에, 이를 확인하지 않으면 엉뚱한 예측 결과가 나올 수 있습니다.
다음 코드를 살펴봅시다.
import numpy as np
import matplotlib.pyplot as plt
# 정상 시계열: 평균과 분산이 일정
np.random.seed(42)
stationary_data = np.random.normal(loc=0, scale=1, size=200)
# 비정상 시계열: 시간에 따라 평균이 증가 (추세 존재)
trend = np.linspace(0, 10, 200)
non_stationary_data = trend + np.random.normal(loc=0, scale=1, size=200)
# 두 시계열 비교 시각화
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(stationary_data)
axes[0].set_title('Stationary Series')
axes[1].plot(non_stationary_data)
axes[1].set_title('Non-Stationary Series')
plt.show()
김개발 씨는 입사 6개월 차 데이터 분석가입니다. 최근 회사에서 매출 예측 프로젝트를 맡게 되었습니다.
열심히 ARIMA 모델을 적용했는데, 예측 결과가 실제 값과 너무 달랐습니다. 도대체 뭐가 문제일까요?
박시니어 씨가 김개발 씨의 코드를 살펴보더니 말했습니다. "데이터가 정상성을 만족하는지 확인해 봤어요?" 김개발 씨는 고개를 갸우뚱했습니다.
정상성이라니, 처음 듣는 용어였습니다. 그렇다면 정상성이란 정확히 무엇일까요?
쉽게 비유하자면, 정상성은 마치 체온과 같습니다. 건강한 사람의 체온은 아침이든 저녁이든 대략 36.5도 근처를 유지합니다.
약간의 변동은 있지만, 갑자기 30도로 떨어지거나 45도로 치솟지 않습니다. 이렇게 평균값이 일정하게 유지되는 상태가 바로 정상성입니다.
반면, 아이의 키를 생각해 보세요. 시간이 지나면 계속 커집니다.
평균값 자체가 시간에 따라 변하는 것입니다. 이런 데이터는 비정상 시계열이라고 부릅니다.
정상성이 왜 중요할까요? 대부분의 시계열 예측 모델은 데이터가 정상성을 만족한다고 가정합니다.
ARIMA, SARIMA 같은 전통적인 모델뿐 아니라, 많은 머신러닝 기법들도 마찬가지입니다. 비정상 데이터를 그대로 넣으면 모델이 데이터의 패턴을 제대로 학습하지 못합니다.
위 코드를 살펴보겠습니다. 첫 번째 그래프는 정상 시계열입니다.
평균이 0 근처에서 일정하게 유지되고, 위아래로 흔들리는 폭도 비슷합니다. 반면 두 번째 그래프는 시간이 지날수록 값이 커집니다.
이것이 **추세(trend)**가 있는 비정상 시계열입니다. 실제 현업에서 만나는 데이터는 대부분 비정상입니다.
주가, 매출액, 웹사이트 방문자 수 모두 시간에 따라 증가하거나 감소하는 추세를 보입니다. 따라서 이런 데이터를 분석하기 전에 반드시 정상성 검정을 수행하고, 필요하다면 변환 작업을 거쳐야 합니다.
정상성에는 강정상성과 약정상성 두 가지가 있습니다. 강정상성은 모든 통계적 특성이 시간에 불변하는 것이고, 약정상성은 평균과 분산만 일정하면 됩니다.
실무에서는 대부분 약정상성을 기준으로 합니다. 다시 김개발 씨 이야기로 돌아가 봅시다.
박시니어 씨의 조언을 듣고 데이터를 확인해 보니, 매출 데이터에 뚜렷한 상승 추세가 있었습니다. 정상성을 확보한 후 다시 모델을 돌리니, 예측 정확도가 크게 향상되었습니다.
실전 팁
💡 - 시계열 분석을 시작하기 전에 항상 데이터를 시각화하여 추세나 계절성이 있는지 먼저 확인하세요
- 눈으로 보는 것만으로는 부족하므로, 통계적 검정을 함께 수행해야 합니다
2. ADF 검정으로 정상성 확인하기
김개발 씨는 그래프를 보면서 "이 데이터가 정상인 것 같기도 하고 아닌 것 같기도 하고..." 하며 고민에 빠졌습니다. 박시니어 씨가 웃으며 말했습니다.
"눈대중으로 판단하지 말고, ADF 검정을 돌려봐요. 숫자가 답을 알려줄 거예요."
**ADF 검정(Augmented Dickey-Fuller Test)**은 시계열 데이터가 정상성을 만족하는지 통계적으로 판단하는 방법입니다. 마치 병원에서 혈액 검사로 건강 상태를 수치로 확인하는 것처럼, ADF 검정은 데이터의 정상성을 p-value라는 숫자로 알려줍니다.
다음 코드를 살펴봅시다.
from statsmodels.tsa.stattools import adfuller
import pandas as pd
def adf_test(series, name=''):
"""ADF 검정을 수행하고 결과를 출력하는 함수"""
result = adfuller(series, autolag='AIC')
print(f'=== {name} ADF 검정 결과 ===')
print(f'ADF 통계량: {result[0]:.4f}')
print(f'p-value: {result[1]:.4f}')
print(f'사용된 지연 수: {result[2]}')
print(f'관측치 수: {result[3]}')
# p-value가 0.05보다 작으면 정상성 만족
if result[1] <= 0.05:
print('결론: 정상 시계열입니다 (귀무가설 기각)')
else:
print('결론: 비정상 시계열입니다 (귀무가설 채택)')
return result[1]
# 정상 시계열 검정
p_value = adf_test(stationary_data, '정상 시계열')
김개발 씨는 매출 데이터 그래프를 한참 들여다보고 있었습니다. 어떤 부분은 안정적으로 보이고, 어떤 부분은 불안정해 보였습니다.
주관적인 판단으로는 확신이 서지 않았습니다. 이때 박시니어 씨가 알려준 것이 바로 ADF 검정입니다.
ADF는 Augmented Dickey-Fuller의 약자로, 시계열 분석에서 가장 널리 사용되는 정상성 검정 방법입니다. ADF 검정의 원리를 쉽게 설명해 보겠습니다.
마치 재판과 비슷합니다. 검찰(귀무가설)은 "이 데이터는 비정상이다"라고 주장합니다.
변호인(대립가설)은 "아니다, 정상이다"라고 반박합니다. ADF 검정은 증거(데이터)를 분석해서 누구의 손을 들어줄지 결정합니다.
p-value가 바로 그 판결문입니다. p-value가 0.05보다 작으면, 검찰의 주장을 기각합니다.
즉, 데이터가 정상 시계열이라는 뜻입니다. 반대로 p-value가 0.05보다 크면, 검찰의 주장을 받아들입니다.
데이터가 비정상이라는 의미입니다. 위 코드를 살펴보겠습니다.
adfuller 함수가 핵심입니다. 이 함수에 시계열 데이터를 넣으면 여러 가지 결과를 반환합니다.
그중 두 번째 값인 result[1]이 바로 p-value입니다. autolag='AIC' 옵션은 적절한 지연(lag) 수를 자동으로 선택하라는 의미입니다.
AIC는 모델 선택 기준 중 하나로, 이 옵션을 사용하면 검정의 정확도가 높아집니다. 실무에서는 ADF 검정만 믿으면 안 됩니다.
KPSS 검정이라는 방법도 함께 사용하는 것이 좋습니다. ADF와 KPSS의 귀무가설이 서로 반대이기 때문에, 두 검정을 함께 사용하면 더 확실한 결론을 내릴 수 있습니다.
한 가지 주의할 점이 있습니다. 데이터의 개수가 너무 적으면 ADF 검정의 신뢰도가 떨어집니다.
일반적으로 최소 50개 이상의 데이터 포인트가 있어야 의미 있는 결과를 얻을 수 있습니다. 김개발 씨는 매출 데이터에 ADF 검정을 돌려봤습니다.
p-value가 0.32로 나왔습니다. 0.05보다 훨씬 크니, 이 데이터는 비정상 시계열이 확실했습니다.
이제 어떻게 해야 할까요? 바로 다음에 배울 차분이 해답입니다.
실전 팁
💡 - p-value 기준은 보통 0.05를 사용하지만, 더 엄격하게 0.01을 사용하기도 합니다
- ADF 검정과 KPSS 검정을 함께 사용하면 더 신뢰할 수 있는 결과를 얻을 수 있습니다
3. 차분으로 정상성 확보하기
ADF 검정 결과 비정상 시계열로 판명된 매출 데이터를 앞에 두고 김개발 씨가 한숨을 쉬었습니다. "데이터가 비정상이면 어떻게 해요?" 박시니어 씨가 답했습니다.
"걱정 마요, 차분이라는 마법이 있거든요."
**차분(Differencing)**은 시계열에서 연속된 값들의 차이를 구하는 방법입니다. 마치 매일 몸무게를 재면서 "어제보다 얼마나 변했는지"를 기록하는 것과 같습니다.
이렇게 하면 상승 추세가 있는 데이터도 안정적인 형태로 바뀝니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
# 비정상 시계열 생성 (상승 추세)
np.random.seed(42)
dates = pd.date_range('2023-01-01', periods=100, freq='D')
trend = np.linspace(100, 200, 100)
sales = trend + np.random.normal(0, 5, 100)
df = pd.DataFrame({'date': dates, 'sales': sales})
# 1차 차분 적용
df['diff_1'] = df['sales'].diff()
# 차분 전후 비교
print('원본 데이터 처음 5개:')
print(df['sales'].head())
print('\n1차 차분 후 데이터:')
print(df['diff_1'].head())
# 차분 후 ADF 검정
diff_data = df['diff_1'].dropna()
adf_test(diff_data, '1차 차분 후')
박시니어 씨가 화이트보드에 숫자를 적기 시작했습니다. "월요일 매출 100만원, 화요일 110만원, 수요일 125만원이라고 해봐요." 김개발 씨가 고개를 끄덕였습니다.
"원본 데이터를 보면 계속 올라가죠? 이게 추세예요.
그런데 차분을 하면 어떻게 될까요?" 박시니어 씨가 계산했습니다. 화요일 차분값은 110 - 100 = 10만원.
수요일 차분값은 125 - 110 = 15만원. "보세요, 이제 '변화량'만 남았어요.
추세는 사라지고, 매일 얼마나 변했는지만 보이죠." 이것이 바로 1차 차분의 원리입니다. 현재 값에서 바로 이전 값을 빼는 것입니다.
수식으로 표현하면 Y'(t) = Y(t) - Y(t-1)입니다. pandas에서는 diff() 메서드 하나로 간단하게 차분을 적용할 수 있습니다.
위 코드에서 df['sales'].diff()가 바로 그것입니다. 첫 번째 값은 이전 값이 없으므로 NaN이 됩니다.
따라서 분석할 때는 dropna()로 결측값을 제거해야 합니다. 1차 차분으로도 정상성이 확보되지 않으면 어떻게 할까요?
그때는 2차 차분을 적용합니다. 1차 차분한 결과에 다시 한번 차분을 하는 것입니다.
대부분의 실무 데이터는 2차 차분 이내에 정상성이 확보됩니다. 하지만 과도한 차분은 오히려 문제를 일으킵니다.
차분을 너무 많이 하면 데이터가 가진 중요한 정보까지 사라질 수 있습니다. ARIMA 모델에서 d 파라미터가 바로 이 차분 횟수를 나타내는데, 보통 0, 1, 2 중 하나를 사용합니다.
차분의 단점도 있습니다. 차분을 하면 데이터 포인트가 하나씩 줄어듭니다.
1차 차분을 하면 1개, 2차 차분을 하면 2개가 줄어듭니다. 데이터가 적을 때는 이것이 문제가 될 수 있습니다.
또한 차분한 데이터로 예측한 결과는 다시 원래 스케일로 복원해야 합니다. 이 과정을 역차분이라고 합니다.
cumsum() 함수를 사용하면 됩니다. 김개발 씨가 매출 데이터에 1차 차분을 적용하고 다시 ADF 검정을 돌렸습니다.
p-value가 0.0001로 나왔습니다. 드디어 정상성을 확보한 것입니다.
실전 팁
💡 - 보통 1차 또는 2차 차분이면 충분합니다. 3차 이상의 차분은 거의 필요하지 않습니다
- 차분 후에는 반드시 ADF 검정으로 정상성이 확보되었는지 확인하세요
4. 로그 변환으로 분산 안정화하기
김개발 씨가 다른 데이터셋을 분석하다가 또 막혔습니다. "이 데이터는 차분을 해도 그래프가 이상해요.
처음에는 조용하다가 뒤로 갈수록 막 요동치는데요?" 박시니어 씨가 그래프를 보더니 말했습니다. "아, 이건 분산이 커지는 경우네요.
로그 변환이 필요해요."
**로그 변환(Log Transformation)**은 데이터에 로그를 취해서 분산을 안정화시키는 방법입니다. 마치 소리가 점점 커지는 것을 볼륨 조절로 일정하게 만드는 것과 같습니다.
특히 시간이 지남에 따라 변동폭이 커지는 데이터에 효과적입니다.
다음 코드를 살펴봅시다.
import numpy as np
import pandas as pd
# 분산이 증가하는 비정상 시계열 생성
np.random.seed(42)
time = np.arange(1, 201)
# 지수적으로 증가하면서 변동성도 커지는 데이터
raw_data = np.exp(0.02 * time) * (1 + 0.1 * np.random.randn(200))
# 로그 변환 적용
log_data = np.log(raw_data)
# 변환 전후 비교
print('원본 데이터 통계:')
print(f'처음 50개 표준편차: {np.std(raw_data[:50]):.2f}')
print(f'마지막 50개 표준편차: {np.std(raw_data[-50:]):.2f}')
print('\n로그 변환 후 통계:')
print(f'처음 50개 표준편차: {np.std(log_data[:50]):.4f}')
print(f'마지막 50개 표준편차: {np.std(log_data[-50:]):.4f}')
박시니어 씨가 설명을 시작했습니다. "정상성에는 두 가지 조건이 있어요.
첫째는 평균이 일정해야 하고, 둘째는 분산이 일정해야 해요." 김개발 씨가 고개를 끄덕였습니다. "아, 차분은 평균을 일정하게 만드는 거고, 로그 변환은 분산을 일정하게 만드는 거군요?" "정확해요!" 박시니어 씨가 미소 지었습니다.
로그 변환의 원리를 이해하기 위해 간단한 예를 들어보겠습니다. 주식 가격이 100원일 때 10% 변동하면 10원입니다.
하지만 같은 주식이 10,000원이 됐을 때 10% 변동하면 1,000원입니다. 절대적인 변동폭은 100배 차이가 납니다.
이런 데이터를 그래프로 그리면, 뒤로 갈수록 변동폭이 커 보입니다. 하지만 실제로는 같은 10% 변동인 것입니다.
로그 변환을 하면 이 비율적 변화가 절대적 변화로 바뀝니다. 수학적으로 설명하면, log(110) - log(100) = log(1.1)이고, log(11000) - log(10000) = log(1.1)입니다.
로그를 취하면 같은 비율의 변화는 같은 차이로 나타납니다. 위 코드를 보면, 원본 데이터는 처음 50개의 표준편차와 마지막 50개의 표준편차가 크게 다릅니다.
분산이 시간에 따라 증가한다는 뜻입니다. 하지만 로그 변환 후에는 두 구간의 표준편차가 비슷해집니다.
실무에서는 로그 변환 후 차분을 적용하는 경우가 많습니다. 먼저 로그로 분산을 안정화하고, 차분으로 추세를 제거하는 것입니다.
이 조합은 주가, 환율, 매출액 같은 경제 데이터 분석에서 매우 흔하게 사용됩니다. 주의할 점이 있습니다.
로그 변환은 양수 데이터에만 적용할 수 있습니다. 0이나 음수가 포함된 데이터에는 np.log1p() 함수를 사용하거나, 상수를 더해서 양수로 만든 후 변환해야 합니다.
또한 로그 변환한 데이터로 예측한 결과는 np.exp()로 다시 원래 스케일로 복원해야 합니다. 이 과정을 잊으면 결과 해석에 오류가 생깁니다.
김개발 씨는 문제의 데이터에 로그 변환을 적용한 후 차분까지 했습니다. 드디어 깔끔하게 정상성을 확보했습니다.
"이제 모델을 돌릴 수 있겠네요!"
실전 팁
💡 - 로그 변환 후 차분을 적용하는 순서가 일반적입니다. 순서를 바꾸면 결과가 달라질 수 있습니다
- 데이터에 0이나 음수가 있으면 np.log1p()를 사용하거나 상수를 더해주세요
5. Box-Cox 변환
김개발 씨가 로그 변환을 적용했는데 결과가 썩 좋지 않았습니다. "로그 변환이 만능은 아닌가 봐요?" 박시니어 씨가 고개를 끄덕였습니다.
"맞아요, 그럴 때는 Box-Cox 변환을 써보세요. 최적의 변환을 자동으로 찾아줍니다."
Box-Cox 변환은 데이터를 정규분포에 가깝게 만들어주는 거듭제곱 변환입니다. 마치 맞춤 양복처럼, 데이터에 딱 맞는 변환 파라미터를 자동으로 찾아줍니다.
로그 변환은 Box-Cox 변환의 특수한 경우(람다=0)에 해당합니다.
다음 코드를 살펴봅시다.
from scipy import stats
from scipy.stats import boxcox
import numpy as np
# 비정규 분포 데이터 생성
np.random.seed(42)
skewed_data = np.random.exponential(scale=2, size=1000)
# Box-Cox 변환 적용 (양수 데이터만 가능)
transformed_data, lambda_param = boxcox(skewed_data)
print(f'최적의 람다 값: {lambda_param:.4f}')
print(f'\n변환 전 왜도: {stats.skew(skewed_data):.4f}')
print(f'변환 후 왜도: {stats.skew(transformed_data):.4f}')
# 람다 값에 따른 변환 해석
if abs(lambda_param) < 0.1:
print('람다가 0에 가까움 -> 로그 변환과 유사')
elif abs(lambda_param - 0.5) < 0.1:
print('람다가 0.5에 가까움 -> 제곱근 변환과 유사')
elif abs(lambda_param - 1) < 0.1:
print('람다가 1에 가까움 -> 변환 불필요')
박시니어 씨가 화이트보드에 수식을 적었습니다. "Box-Cox 변환의 핵심은 람다라는 파라미터예요." 람다 값에 따라 변환이 달라집니다.
람다가 1이면 변환 없음, 0이면 로그 변환, 0.5면 제곱근 변환, -1이면 역수 변환입니다. Box-Cox는 이 중에서 데이터를 가장 정규분포에 가깝게 만드는 람다를 찾아줍니다.
쉽게 비유하자면, 로그 변환은 기성복이고 Box-Cox는 맞춤복입니다. 기성복이 잘 맞으면 좋지만, 안 맞으면 맞춤복을 입어야 합니다.
Box-Cox는 데이터의 특성을 분석해서 가장 잘 맞는 변환을 알아서 찾아줍니다. 위 코드에서 boxcox() 함수는 두 가지를 반환합니다.
첫 번째는 변환된 데이터이고, 두 번째는 최적의 람다 값입니다. 이 람다 값을 저장해두었다가 나중에 예측 결과를 원래 스케일로 복원할 때 사용해야 합니다.
**왜도(skewness)**는 분포가 얼마나 비대칭인지를 나타내는 지표입니다. 0에 가까울수록 좌우 대칭, 즉 정규분포에 가깝습니다.
코드 결과를 보면 변환 전에는 왜도가 큰 양수였지만, 변환 후에는 0에 가까워진 것을 확인할 수 있습니다. Box-Cox 변환에도 제약이 있습니다.
반드시 양수 데이터에만 적용할 수 있습니다. 0이나 음수가 있으면 오류가 발생합니다.
이런 경우에는 Yeo-Johnson 변환을 사용하면 됩니다. scipy.stats.yeojohnson() 함수로 사용할 수 있으며, 음수 데이터도 처리할 수 있습니다.
실무에서 Box-Cox 변환을 사용할 때 주의할 점이 있습니다. 훈련 데이터에서 구한 람다 값을 테스트 데이터에도 동일하게 적용해야 합니다.
테스트 데이터에 대해 새로운 람다를 구하면 데이터 누수가 발생합니다. 역변환 공식도 알아두면 좋습니다.
scipy.special.inv_boxcox() 함수를 사용하거나, 람다가 0이 아닐 때는 (y * lambda + 1) ** (1/lambda) 공식으로 복원할 수 있습니다. 김개발 씨가 문제의 데이터에 Box-Cox 변환을 적용했더니 람다가 0.25로 나왔습니다.
로그 변환도 제곱근 변환도 아닌, 그 중간쯤의 변환이 최적이었던 것입니다. "역시 맞춤이 최고네요!"
실전 팁
💡 - Box-Cox 변환 후에는 람다 값을 반드시 저장해두세요. 역변환할 때 필요합니다
- 음수 데이터가 있으면 Yeo-Johnson 변환(scipy.stats.yeojohnson)을 사용하세요
6. 계절성 제거를 위한 계절 차분
김개발 씨가 에어컨 판매 데이터를 분석하고 있었습니다. 1차 차분을 해도 ADF 검정을 통과하지 못했습니다.
그래프를 보니 여름마다 튀어오르는 패턴이 반복되고 있었습니다. "이건 뭔가요?" 박시니어 씨가 답했습니다.
"계절성이에요. 계절 차분이 필요합니다."
**계절 차분(Seasonal Differencing)**은 같은 시점의 이전 주기 값과의 차이를 구하는 방법입니다. 마치 올해 7월 매출과 작년 7월 매출을 비교하는 것과 같습니다.
일반 차분이 추세를 제거한다면, 계절 차분은 반복되는 계절 패턴을 제거합니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
# 계절성이 있는 데이터 생성 (월별 데이터, 12개월 주기)
np.random.seed(42)
months = 48 # 4년치 데이터
time = np.arange(months)
# 추세 + 계절성 + 노이즈
trend = 0.5 * time
seasonality = 10 * np.sin(2 * np.pi * time / 12) # 12개월 주기
noise = np.random.normal(0, 2, months)
sales = 100 + trend + seasonality + noise
df = pd.DataFrame({'sales': sales})
# 일반 1차 차분 (추세 제거)
df['diff_1'] = df['sales'].diff(1)
# 계절 차분 (12개월 주기 계절성 제거)
df['seasonal_diff'] = df['sales'].diff(12)
# 일반 차분 + 계절 차분 (둘 다 적용)
df['both_diff'] = df['sales'].diff(12).diff(1)
print('원본 데이터 ADF p-value:', adfuller(df['sales'].dropna())[1])
print('1차 차분 후 p-value:', adfuller(df['diff_1'].dropna())[1])
print('계절 차분 후 p-value:', adfuller(df['seasonal_diff'].dropna())[1])
print('둘 다 적용 후 p-value:', adfuller(df['both_diff'].dropna())[1])
박시니어 씨가 에어컨 판매 그래프를 가리켰습니다. "보세요, 매년 7월에 뾰족하게 올라가고 1월에 푹 꺼지죠?
이게 계절성이에요." **계절성(Seasonality)**은 일정한 주기로 반복되는 패턴입니다. 에어컨은 여름에 잘 팔리고, 아이스크림도 여름에 잘 팔립니다.
반대로 난방용품은 겨울에 잘 팔립니다. 이런 패턴은 매년 반복됩니다.
일반 차분은 바로 이전 값과의 차이를 구합니다. 7월 값에서 6월 값을 빼는 식입니다.
하지만 계절 차분은 다릅니다. 올해 7월 값에서 작년 7월 값을 뺍니다.
같은 계절끼리 비교하는 것입니다. 수식으로 표현하면, 월별 데이터의 계절 차분은 Y'(t) = Y(t) - Y(t-12)입니다.
12개월 전 값을 빼는 것이죠. pandas에서는 diff(12)로 간단하게 구현할 수 있습니다.
위 코드를 보면 세 가지 경우를 비교합니다. 첫째, 1차 차분만 적용한 경우입니다.
추세는 제거되지만 계절성은 남아있어서 여전히 정상성을 만족하지 못할 수 있습니다. 둘째, 계절 차분만 적용한 경우입니다.
계절성은 제거되지만 추세가 남아있을 수 있습니다. 셋째, 둘 다 적용한 경우입니다.
추세와 계절성이 모두 제거되어 정상성을 확보할 가능성이 가장 높습니다. 실무에서 계절 주기를 어떻게 알 수 있을까요?
데이터의 특성에 따라 다릅니다. 월별 데이터는 보통 12개월 주기, 주별 데이터는 52주 주기, 일별 데이터는 7일(주간) 또는 365일(연간) 주기를 고려합니다.
주기를 모르겠다면 자기상관함수(ACF) 그래프를 그려보세요. 주기마다 튀어오르는 패턴이 보일 것입니다.
statsmodels의 plot_acf() 함수로 쉽게 확인할 수 있습니다. SARIMA 모델에서는 이 계절 차분 횟수를 D 파라미터로 지정합니다.
대문자 D는 계절 차분, 소문자 d는 일반 차분을 의미합니다. 계절 차분의 단점은 데이터 손실이 크다는 것입니다.
12개월 주기 계절 차분을 하면 처음 12개의 데이터 포인트를 잃습니다. 데이터가 충분하지 않으면 이것이 문제가 될 수 있습니다.
김개발 씨는 에어컨 판매 데이터에 계절 차분과 1차 차분을 함께 적용했습니다. 드디어 ADF 검정을 통과했습니다.
"이제 SARIMA 모델을 적용할 수 있겠네요!"
실전 팁
💡 - 계절 주기를 모르면 ACF 그래프를 그려서 주기적으로 튀어오르는 지점을 확인하세요
- 계절 차분과 일반 차분을 함께 적용할 때는 계절 차분을 먼저 하는 것이 일반적입니다
7. KPSS 검정으로 이중 확인하기
김개발 씨가 ADF 검정 결과를 가지고 회의에 참석했습니다. 그런데 선임 분석가가 물었습니다.
"KPSS 검정도 해봤어요?" 김개발 씨가 당황했습니다. ADF 검정만 하면 되는 줄 알았는데, 또 다른 검정이 있었던 것입니다.
**KPSS 검정(Kwiatkowski-Phillips-Schmidt-Shin Test)**은 ADF 검정과 반대되는 귀무가설을 가진 정상성 검정 방법입니다. ADF가 "비정상이다"를 귀무가설로 하는 반면, KPSS는 "정상이다"를 귀무가설로 합니다.
두 검정을 함께 사용하면 더 확실한 결론을 내릴 수 있습니다.
다음 코드를 살펴봅시다.
from statsmodels.tsa.stattools import adfuller, kpss
import numpy as np
import warnings
warnings.filterwarnings('ignore')
def stationarity_test(series, name=''):
"""ADF와 KPSS 검정을 함께 수행"""
print(f'=== {name} 정상성 검정 ===\n')
# ADF 검정
adf_result = adfuller(series, autolag='AIC')
adf_pvalue = adf_result[1]
# KPSS 검정
kpss_result = kpss(series, regression='c', nlags='auto')
kpss_pvalue = kpss_result[1]
print(f'ADF p-value: {adf_pvalue:.4f}')
print(f'KPSS p-value: {kpss_pvalue:.4f}\n')
# 결과 해석
if adf_pvalue <= 0.05 and kpss_pvalue > 0.05:
print('결론: 정상 시계열 (두 검정 모두 정상성 지지)')
elif adf_pvalue > 0.05 and kpss_pvalue <= 0.05:
print('결론: 비정상 시계열 (두 검정 모두 비정상성 지지)')
elif adf_pvalue <= 0.05 and kpss_pvalue <= 0.05:
print('결론: 추세 정상 가능성 (차분 필요)')
else:
print('결론: 판단 어려움 (추가 분석 필요)')
# 테스트
np.random.seed(42)
stationary = np.random.normal(0, 1, 200)
stationarity_test(stationary, '정상 시계열')
박시니어 씨가 설명했습니다. "ADF 검정 하나만 믿으면 안 돼요.
통계 검정에는 항상 오류 가능성이 있거든요." 제1종 오류는 실제로는 정상인데 비정상이라고 잘못 판단하는 것입니다. 제2종 오류는 반대로 비정상인데 정상이라고 잘못 판단하는 것입니다.
어떤 검정이든 이 두 가지 오류에서 완전히 자유로울 수 없습니다. 그래서 실무에서는 ADF와 KPSS 두 가지 검정을 함께 사용합니다.
두 검정의 귀무가설이 정반대이기 때문에, 함께 사용하면 더 신뢰할 수 있는 결론을 내릴 수 있습니다. ADF 검정의 귀무가설은 "데이터가 비정상이다"입니다.
p-value가 0.05보다 작으면 귀무가설을 기각하고, 데이터가 정상이라고 판단합니다. KPSS 검정의 귀무가설은 "데이터가 정상이다"입니다.
p-value가 0.05보다 작으면 귀무가설을 기각하고, 데이터가 비정상이라고 판단합니다. 두 검정 결과를 조합하면 네 가지 경우가 나옵니다.
첫째, ADF가 정상이고 KPSS도 정상이면 확실히 정상 시계열입니다. 둘째, ADF가 비정상이고 KPSS도 비정상이면 확실히 비정상 시계열입니다.
셋째, ADF는 정상인데 KPSS는 비정상인 경우입니다. 이것은 추세 정상(trend stationary) 시계열일 가능성이 있습니다.
결정론적 추세를 제거하거나 차분이 필요합니다. 넷째, ADF는 비정상인데 KPSS는 정상인 경우입니다.
이것은 드문 경우로, 추가적인 분석이 필요합니다. 데이터의 특성을 더 자세히 살펴봐야 합니다.
위 코드의 kpss() 함수에서 regression='c' 옵션은 상수항만 고려한다는 뜻입니다. 추세도 함께 고려하려면 regression='ct'를 사용합니다.
nlags='auto'는 적절한 지연 수를 자동으로 선택합니다. 실무에서 권장하는 절차는 다음과 같습니다.
먼저 ADF와 KPSS를 모두 수행합니다. 둘 다 정상을 가리키면 바로 모델링을 진행합니다.
둘 다 비정상을 가리키면 차분이나 변환을 적용합니다. 결과가 상충하면 데이터를 더 자세히 분석합니다.
김개발 씨는 이제 항상 두 검정을 함께 수행합니다. "혼자 판단하는 것보다 두 명이 판단하는 게 더 정확하듯이, 검정도 두 개가 더 믿음직스럽네요!"
실전 팁
💡 - ADF p-value < 0.05이고 KPSS p-value > 0.05면 정상 시계열로 확신할 수 있습니다
- 두 검정 결과가 상충하면 데이터 시각화와 추가 분석으로 판단하세요
8. 실전 파이프라인 구축하기
김개발 씨가 지금까지 배운 것들을 실제 프로젝트에 적용하려고 합니다. 그런데 막상 하려니 어디서부터 시작해야 할지 막막했습니다.
박시니어 씨가 말했습니다. "정상성 검정과 변환을 체계적으로 수행하는 파이프라인을 만들어 봐요."
정상성 파이프라인은 시계열 데이터의 정상성을 검정하고 필요한 변환을 자동으로 적용하는 일련의 과정입니다. 마치 공장의 생산 라인처럼, 데이터가 들어오면 정해진 순서대로 검사하고 가공해서 분석 가능한 형태로 만들어줍니다.
다음 코드를 살펴봅시다.
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import adfuller, kpss
from scipy.stats import boxcox
import warnings
warnings.filterwarnings('ignore')
class StationarityPipeline:
"""정상성 검정 및 변환 파이프라인"""
def __init__(self, significance=0.05):
self.significance = significance
self.transformations = []
self.lambda_param = None
def check_stationarity(self, data):
"""ADF + KPSS 이중 검정"""
adf_pval = adfuller(data, autolag='AIC')[1]
kpss_pval = kpss(data, regression='c', nlags='auto')[1]
is_stationary = (adf_pval <= self.significance and
kpss_pval > self.significance)
return is_stationary, adf_pval, kpss_pval
def fit_transform(self, data, max_diff=2):
"""정상성 확보를 위한 자동 변환"""
result = data.copy()
# 1. 분산 안정화 (Box-Cox)
if np.all(result > 0):
result, self.lambda_param = boxcox(result)
self.transformations.append('boxcox')
# 2. 차분 적용 (최대 max_diff번)
for i in range(max_diff):
is_stat, adf_p, kpss_p = self.check_stationarity(result)
if is_stat:
break
result = pd.Series(result).diff().dropna().values
self.transformations.append(f'diff_{i+1}')
print(f'적용된 변환: {self.transformations}')
return result
# 사용 예시
pipeline = StationarityPipeline()
transformed = pipeline.fit_transform(non_stationary_data)
박시니어 씨가 말했습니다. "매번 수동으로 검정하고 변환하면 실수하기 쉬워요.
파이프라인을 만들어 두면 일관성 있게 처리할 수 있습니다." 위 코드는 정상성 검정과 변환을 자동화한 클래스입니다. 핵심 로직을 단계별로 살펴보겠습니다.
먼저 check_stationarity 메서드는 ADF와 KPSS 검정을 함께 수행합니다. 두 검정 모두 정상성을 지지할 때만 True를 반환합니다.
하나라도 비정상을 가리키면 False입니다. fit_transform 메서드가 핵심입니다.
첫 번째 단계에서 Box-Cox 변환을 적용해 분산을 안정화합니다. 단, 데이터가 모두 양수일 때만 적용합니다.
람다 값은 나중에 역변환할 때 필요하므로 저장해둡니다. 두 번째 단계에서는 차분을 적용합니다.
한 번 차분하고 정상성을 검사합니다. 아직 비정상이면 한 번 더 차분합니다.
최대 max_diff번까지 반복합니다. transformations 리스트에는 적용된 변환이 순서대로 기록됩니다.
이 정보는 나중에 예측 결과를 원래 스케일로 복원할 때 필수적입니다. 역순으로 역변환을 적용해야 하기 때문입니다.
실무에서는 이 파이프라인을 더 확장할 수 있습니다. 예를 들어, 계절 차분을 추가하거나, 이상치 처리 로직을 넣거나, 변환 결과를 시각화하는 기능을 추가할 수 있습니다.
sklearn의 Pipeline이나 TransformerMixin을 상속받아 구현하면 더 유연하게 사용할 수 있습니다. fit()과 transform()을 분리하면 학습 데이터와 테스트 데이터를 일관되게 처리할 수 있습니다.
주의할 점이 있습니다. 테스트 데이터에는 fit_transform이 아닌 transform만 적용해야 합니다.
학습 데이터에서 구한 람다 값과 차분 횟수를 그대로 사용해야 데이터 누수를 방지할 수 있습니다. 역변환 메서드도 구현해두면 좋습니다.
예측 결과를 원래 스케일로 복원하려면 차분의 역변환(cumsum)과 Box-Cox의 역변환(inv_boxcox)을 역순으로 적용해야 합니다. 김개발 씨는 이 파이프라인을 자신의 프로젝트에 적용했습니다.
"이제 새로운 데이터가 들어와도 일관되게 처리할 수 있겠네요!"
실전 팁
💡 - 적용된 변환 목록을 반드시 저장해두세요. 역변환 시 역순으로 적용해야 합니다
- sklearn의 Pipeline과 호환되게 구현하면 머신러닝 워크플로우에 쉽게 통합할 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.