🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

시계열 패턴 분석: 추세, 계절성, 주기성 - 슬라이드 1/7
A

AI Generated

2025. 12. 3. · 16 Views

시계열 패턴 분석: 추세, 계절성, 주기성

시계열 데이터에 숨겨진 세 가지 핵심 패턴인 추세, 계절성, 주기성을 파이썬으로 분석하는 방법을 알아봅니다. 실무에서 매출 예측, 재고 관리, 트래픽 분석에 필수적인 기법들을 초급자도 이해할 수 있도록 설명합니다.


목차

  1. 시계열_데이터의_기본_이해
  2. 추세_분석_Trend_Analysis
  3. 계절성_분석_Seasonality_Analysis
  4. 주기성과_계절성의_차이
  5. STL_분해로_완전한_분석하기
  6. 패턴_기반_예측_실전

1. 시계열 데이터의 기본 이해

김개발 씨는 이커머스 회사에 입사한 지 한 달 된 주니어 데이터 분석가입니다. 어느 날 팀장님이 다가와 말했습니다.

"김개발 씨, 지난 3년간의 매출 데이터를 분석해서 다음 달 매출을 예측해볼 수 있겠어요?" 김개발 씨는 엑셀 파일을 열어봤지만, 숫자의 바다 속에서 어디서부터 시작해야 할지 막막했습니다.

시계열 데이터란 시간 순서에 따라 기록된 데이터를 말합니다. 마치 일기장에 매일 날씨를 기록하는 것처럼, 특정 시점마다 측정된 값들이 줄지어 있는 형태입니다.

주식 가격, 기온 변화, 웹사이트 방문자 수 등이 모두 시계열 데이터에 해당합니다. 시계열 분석을 제대로 이해하면 미래를 예측하고 비즈니스 의사결정에 활용할 수 있습니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 시계열 데이터 생성 및 기본 구조 확인
dates = pd.date_range(start='2022-01-01', periods=365, freq='D')
# 랜덤 시드 설정으로 재현 가능한 데이터 생성
np.random.seed(42)
sales = 100 + np.random.randn(365).cumsum()

# 시계열 데이터프레임 생성
ts_data = pd.DataFrame({'date': dates, 'sales': sales})
ts_data.set_index('date', inplace=True)

# 기본 통계 확인
print(ts_data.describe())
print(f"데이터 기간: {ts_data.index.min()} ~ {ts_data.index.max()}")

김개발 씨는 입사 한 달 차 주니어 데이터 분석가입니다. 오늘 팀장님으로부터 매출 예측이라는 첫 번째 미션을 받았습니다.

3년치 매출 데이터가 담긴 엑셀 파일을 열어보니 날짜와 숫자가 끝없이 나열되어 있었습니다. 선배 분석가 박시니어 씨가 다가와 화면을 살펴봅니다.

"아, 시계열 데이터네요. 이건 일반적인 데이터 분석과는 조금 다른 접근이 필요해요." 그렇다면 시계열 데이터란 정확히 무엇일까요?

쉽게 비유하자면, 시계열 데이터는 마치 성장 앨범과 같습니다. 아이가 태어나서부터 매년 같은 날 찍은 사진을 모아놓으면, 시간에 따른 성장 과정을 한눈에 볼 수 있습니다.

키가 얼마나 자랐는지, 얼굴이 어떻게 변했는지 추적할 수 있지요. 시계열 데이터도 마찬가지로 시간의 흐름에 따른 값의 변화를 담고 있습니다.

시계열 분석이 없던 시절에는 어땠을까요? 분석가들은 단순히 평균값을 계산하거나, 최근 몇 개월의 데이터만 보고 감으로 예측해야 했습니다.

"지난달 매출이 1억이었으니 이번 달도 비슷하겠지"라는 식이었습니다. 하지만 이런 방식으로는 계절적 변동이나 장기적인 성장 추세를 놓치기 쉬웠습니다.

바로 이런 문제를 해결하기 위해 시계열 분석 기법이 발전했습니다. 시계열 분석을 사용하면 데이터에 숨겨진 패턴을 발견할 수 있습니다.

또한 과거 데이터를 기반으로 미래를 예측하는 것이 가능해집니다. 무엇보다 "왜 이 시점에 매출이 올랐는지"에 대한 인사이트를 얻을 수 있다는 큰 이점이 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 pd.date_range 함수를 사용해 2022년 1월 1일부터 365일간의 날짜 인덱스를 생성합니다.

이것이 시계열 데이터의 뼈대가 됩니다. 다음으로 **np.random.randn(365).cumsum()**을 통해 랜덤 워크 형태의 매출 데이터를 만듭니다.

누적합을 사용한 이유는 실제 매출처럼 이전 값에 영향을 받는 패턴을 모방하기 위해서입니다. set_index('date', inplace=True) 부분이 핵심입니다.

날짜 컬럼을 인덱스로 설정하면 pandas가 시계열 데이터로 인식하여 다양한 시계열 전용 기능을 사용할 수 있게 됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 쇼핑몰 서비스를 운영한다고 가정해봅시다. 매일 발생하는 주문 데이터를 시계열로 정리하면, 특정 요일이나 시간대에 주문이 몰리는 패턴을 발견할 수 있습니다.

이 정보를 바탕으로 서버 증설 시점이나 마케팅 캠페인 타이밍을 결정할 수 있습니다. 하지만 주의할 점도 있습니다.

초보 분석가들이 흔히 하는 실수 중 하나는 결측치 처리를 소홀히 하는 것입니다. 시계열 데이터에서 중간에 빠진 날짜가 있으면 분석 결과가 왜곡될 수 있습니다.

따라서 데이터를 불러온 직후 결측치 확인과 처리를 반드시 수행해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 먼저 데이터를 시계열 형태로 정리하기 시작했습니다. "이제 데이터의 구조는 이해했어요.

그런데 여기서 어떤 패턴을 찾아야 하나요?"

실전 팁

💡 - 시계열 데이터를 다룰 때는 항상 날짜 컬럼을 인덱스로 설정하세요

  • 데이터 로딩 후 가장 먼저 결측치와 이상치를 확인하는 습관을 들이세요
  • describe()와 plot()으로 데이터의 전반적인 모습을 파악하세요

2. 추세 분석 Trend Analysis

김개발 씨가 매출 그래프를 그려보니 위아래로 출렁거리는 선이 나타났습니다. "이게 오르는 건가요, 내리는 건가요?" 박시니어 씨가 웃으며 답했습니다.

"잔물결에 집중하지 말고, 큰 파도의 방향을 봐야 해요. 그게 바로 추세입니다."

**추세(Trend)**는 시계열 데이터의 장기적인 방향성을 말합니다. 마치 등산할 때 오르막길인지 내리막길인지 판단하는 것과 같습니다.

중간중간 작은 굴곡이 있더라도 전체적으로 올라가고 있다면 상승 추세, 내려가고 있다면 하락 추세입니다. 추세를 파악하면 사업이 성장하고 있는지, 시장이 축소되고 있는지 판단할 수 있습니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np
from scipy import stats

# 샘플 데이터 생성 (상승 추세 + 노이즈)
np.random.seed(42)
days = 365
trend = np.linspace(100, 200, days)  # 100에서 200으로 상승
noise = np.random.randn(days) * 10
sales = trend + noise

# 이동평균으로 추세 추출
df = pd.DataFrame({'sales': sales})
df['MA_7'] = df['sales'].rolling(window=7).mean()   # 7일 이동평균
df['MA_30'] = df['sales'].rolling(window=30).mean() # 30일 이동평균

# 선형 회귀로 추세선 계산
x = np.arange(len(df))
slope, intercept, r_value, p_value, std_err = stats.linregress(x, df['sales'])
df['trend_line'] = intercept + slope * x

print(f"일일 성장률: {slope:.2f}")
print(f"연간 예상 성장: {slope * 365:.2f}")

김개발 씨는 matplotlib으로 매출 그래프를 그려봤습니다. 선이 위아래로 심하게 출렁거려서 도무지 방향을 알 수가 없었습니다.

"매출이 오르는 건가요, 내리는 건가요?" 박시니어 씨가 화면을 가리키며 설명합니다. "지금 보이는 출렁임은 **잡음(noise)**이에요.

진짜 중요한 건 이 잡음을 걷어내고 보이는 큰 흐름, 바로 추세입니다." 그렇다면 추세란 정확히 무엇일까요? 쉽게 비유하자면, 추세는 마치 비행기가 이륙하는 모습과 같습니다.

비행기가 활주로를 달리다 하늘로 올라갈 때, 중간중간 기류 때문에 흔들릴 수 있습니다. 하지만 전체적인 방향은 분명히 위로 향하고 있지요.

이 "위로 향하는 큰 방향"이 바로 추세입니다. 추세 분석이 없다면 어떤 문제가 생길까요?

경영진에게 "이번 달 매출이 지난달보다 5% 떨어졌습니다"라고 보고한다고 가정해봅시다. 이것만 보면 큰 문제처럼 보입니다.

하지만 만약 연간 추세가 30% 상승이고, 이번 달 하락이 일시적인 변동이라면 어떨까요? 추세를 모르면 단기 변동에 과민 반응하게 되고, 잘못된 의사결정을 내릴 수 있습니다.

바로 이런 문제를 해결하기 위해 추세 분석 기법이 필요합니다. **이동평균(Moving Average)**은 가장 직관적인 추세 추출 방법입니다.

7일 이동평균은 최근 7일간의 평균을 계산하여 일별 변동을 부드럽게 만들어줍니다. 30일 이동평균은 더 긴 기간을 평균 내므로 더 부드러운 추세선을 보여줍니다.

위의 코드를 자세히 살펴보겠습니다. **np.linspace(100, 200, days)**는 100에서 시작해서 200까지 365개의 점을 균등하게 생성합니다.

이것이 우리가 만든 인위적인 상승 추세입니다. 여기에 np.random.randn(days) * 10으로 랜덤한 잡음을 더했습니다.

실제 데이터처럼 출렁이면서도 전체적으로는 상승하는 패턴이 만들어집니다. **rolling(window=7).mean()**이 핵심입니다.

이 함수는 각 시점에서 이전 7일간의 평균을 계산합니다. 창문(window)을 데이터 위로 굴리면서(rolling) 평균을 내는 모습을 상상하면 됩니다.

stats.linregress는 선형 회귀를 수행합니다. x축(시간)과 y축(매출) 사이의 관계를 직선으로 표현하는 것입니다.

기울기(slope)가 양수면 상승 추세, 음수면 하락 추세입니다. 실제 현업에서는 이 기법을 어떻게 활용할까요?

스타트업에서 투자 유치를 준비한다고 가정해봅시다. 투자자들은 "월간 활성 사용자(MAU)가 얼마나 성장하고 있나요?"라고 묻습니다.

이때 월별 수치의 등락을 나열하는 것보다, "연평균 성장률(CAGR) 40%입니다"라고 추세를 요약해서 말하는 것이 훨씬 설득력 있습니다. 하지만 주의할 점이 있습니다.

이동평균의 윈도우 크기를 잘못 선택하면 문제가 생깁니다. 윈도우가 너무 작으면 잡음이 충분히 제거되지 않고, 너무 크면 최근 변화에 둔감해집니다.

비즈니스 특성에 맞는 적절한 윈도우 크기를 찾는 것이 중요합니다. 김개발 씨가 30일 이동평균선을 그려보니, 확실히 우상향하는 추세가 보였습니다.

"오, 매출이 꾸준히 성장하고 있네요!" 박시니어 씨가 고개를 끄덕입니다. "좋아요.

이제 다음 단계로 계절성을 살펴볼 차례입니다."

실전 팁

💡 - 이동평균 윈도우 크기는 데이터 특성에 맞게 조절하세요 (일별 데이터면 7일 또는 30일 권장)

  • 선형 회귀의 R-squared 값을 확인하여 추세선이 데이터를 잘 설명하는지 검증하세요
  • 추세가 갑자기 바뀌는 구간이 있다면, 해당 시점에 무슨 일이 있었는지 확인하세요

3. 계절성 분석 Seasonality Analysis

김개발 씨가 추세선을 제거한 데이터를 살펴보니 규칙적으로 반복되는 패턴이 눈에 들어왔습니다. "어?

왜 매년 12월에 매출이 치솟고, 2월에는 뚝 떨어지죠?" 박시니어 씨가 빙그레 웃었습니다. "축하해요.

방금 계절성을 발견한 거예요."

**계절성(Seasonality)**은 일정한 주기로 반복되는 패턴을 말합니다. 마치 사계절이 매년 같은 순서로 돌아오는 것처럼, 데이터에서도 특정 시기가 되면 규칙적으로 높아지거나 낮아지는 현상이 나타납니다.

연간 계절성(크리스마스 시즌 매출 급증), 월간 계절성(월급날 소비 증가), 주간 계절성(주말 트래픽 감소) 등 다양한 주기가 존재합니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np
from statsmodels.tsa.seasonal import seasonal_decompose

# 계절성이 포함된 샘플 데이터 생성
np.random.seed(42)
days = 730  # 2년치 데이터
t = np.arange(days)

# 추세 + 계절성 + 잡음 조합
trend = 100 + 0.05 * t
seasonality = 20 * np.sin(2 * np.pi * t / 365)  # 연간 주기
weekly = 5 * np.sin(2 * np.pi * t / 7)          # 주간 주기
noise = np.random.randn(days) * 3
sales = trend + seasonality + weekly + noise

# 시계열 분해
df = pd.DataFrame({'sales': sales},
                  index=pd.date_range('2022-01-01', periods=days))
result = seasonal_decompose(df['sales'], model='additive', period=365)

# 각 구성요소 확인
print("추세 성분:", result.trend.dropna().head())
print("계절 성분:", result.seasonal.head())
print("잔차 성분:", result.resid.dropna().head())

김개발 씨는 추세 분석을 마치고 뿌듯한 마음으로 데이터를 다시 살펴봤습니다. 그런데 추세선을 따라가다 보니 이상한 점이 발견되었습니다.

특정 시기마다 매출이 추세선 위로 튀어 오르거나, 아래로 처지는 패턴이 반복되고 있었습니다. "이건 우연이 아닌 것 같은데요?" 김개발 씨가 박시니어 씨에게 물었습니다.

"정확해요. 그게 바로 계절성이에요.

비즈니스에서 아주 중요한 패턴이죠." 그렇다면 계절성이란 정확히 무엇일까요? 쉽게 비유하자면, 계절성은 마치 조수간만의 차와 같습니다.

바닷물은 매일 일정한 시간에 밀려왔다 빠져나갑니다. 이 패턴은 달의 위치에 따라 예측 가능하게 반복됩니다.

시계열 데이터의 계절성도 마찬가지로, 특정 주기마다 값이 올라갔다 내려갔다 하는 예측 가능한 패턴입니다. 계절성을 모르면 어떤 일이 벌어질까요?

아이스크림 회사를 운영한다고 가정해봅시다. 1월 매출이 7월보다 현저히 낮습니다.

계절성을 모르는 경영진은 "1월에 마케팅을 더 해야 해!"라고 외칠 수 있습니다. 하지만 아무리 마케팅을 해도 겨울에 아이스크림 수요가 급증하기는 어렵습니다.

계절성을 이해하면 자원을 효율적으로 배분할 수 있습니다. seasonal_decompose 함수는 시계열 분석의 핵심 도구입니다.

이 함수는 시계열 데이터를 세 가지 구성요소로 분해합니다. 추세(Trend), 계절성(Seasonal), **잔차(Residual)**입니다.

마치 음악에서 여러 악기 소리가 섞인 오케스트라 연주를 각 악기별로 분리해서 듣는 것과 같습니다. 코드를 자세히 살펴보겠습니다.

**np.sin(2 * np.pi * t / 365)**는 365일을 한 주기로 하는 사인파를 생성합니다. 이것이 연간 계절성을 시뮬레이션합니다.

여름에 높고 겨울에 낮은 패턴을 표현합니다. **np.sin(2 * np.pi * t / 7)**은 7일 주기의 사인파로, 주간 패턴을 나타냅니다.

model='additive' 파라미터는 덧셈 모델을 의미합니다. 원본 데이터 = 추세 + 계절성 + 잔차 형태로 분해합니다.

만약 계절 변동의 크기가 추세에 비례해서 커진다면 model='multiplicative'(곱셈 모델)를 사용해야 합니다. period=365는 계절성의 주기를 지정합니다.

연간 패턴을 분석하려면 365, 월간 패턴이면 30, 주간 패턴이면 7을 입력합니다. 실제 현업에서는 이 기법을 어떻게 활용할까요?

물류 회사에서 배송 물량을 예측한다고 가정해봅시다. 추석과 설날에는 물량이 폭증합니다.

계절성을 분석하면 이 시기에 얼마나 많은 인력과 차량이 필요한지 미리 계획할 수 있습니다. 반대로 비수기에는 비용을 절감하는 전략을 세울 수 있습니다.

주의할 점도 있습니다. 주기(period)를 잘못 설정하면 엉뚱한 결과가 나옵니다.

일별 데이터인데 period=12를 설정하면, 월간 데이터로 착각하고 잘못된 계절성을 추출합니다. 또한 데이터 기간이 최소 2주기 이상이어야 계절성을 신뢰할 수 있습니다.

1년치 데이터로 연간 계절성을 분석하면 우연의 일치인지 진짜 패턴인지 구분하기 어렵습니다. 김개발 씨가 seasonal_decompose 결과를 시각화해보니, 매년 비슷한 시기에 매출이 오르내리는 패턴이 선명하게 보였습니다.

"와, 12월에 정말 매출이 치솟네요. 연말 특수가 있는 거군요!" 박시니어 씨가 고개를 끄덕입니다.

"맞아요. 이제 한 가지만 더 알면 됩니다.

주기성이에요."

실전 팁

💡 - 계절성 분석 전에 데이터가 최소 2주기 이상인지 확인하세요

  • 덧셈 모델과 곱셈 모델 중 데이터 특성에 맞는 것을 선택하세요
  • 여러 주기(연간, 월간, 주간)가 겹칠 수 있으니 각각 분리해서 분석하세요

4. 주기성과 계절성의 차이

김개발 씨가 고개를 갸웃거립니다. "선배, 주기성이랑 계절성이 뭐가 다른 거예요?

둘 다 반복되는 패턴 아닌가요?" 박시니어 씨가 칠판에 두 개의 그래프를 그리며 답했습니다. "좋은 질문이에요.

비슷해 보이지만 아주 중요한 차이가 있습니다."

**주기성(Cyclicity)**은 계절성과 달리 고정된 주기가 없는 반복 패턴입니다. 마치 경기 호황과 불황이 반복되지만 정확히 몇 년 주기인지 예측하기 어려운 것처럼, 주기성은 대략적인 패턴은 있으나 그 길이가 일정하지 않습니다.

계절성은 달력과 연결되어 예측 가능하지만, 주기성은 외부 요인에 따라 달라집니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np
from scipy.signal import find_peaks

# 불규칙한 주기를 가진 데이터 생성
np.random.seed(42)
days = 1000
t = np.arange(days)

# 불규칙 주기 생성 (경기 순환 시뮬레이션)
cycle_periods = [120, 180, 150, 200, 140]  # 불규칙한 주기들
cycle = np.zeros(days)
pos = 0
for period in cycle_periods:
    end = min(pos + period, days)
    cycle[pos:end] = 30 * np.sin(np.linspace(0, np.pi, end - pos))
    pos = end
    if pos >= days:
        break

sales = 100 + cycle + np.random.randn(days) * 5

# 피크(고점) 탐지로 주기 분석
df = pd.DataFrame({'sales': sales})
peaks, _ = find_peaks(df['sales'], distance=50, prominence=10)

# 주기 간격 계산
if len(peaks) > 1:
    intervals = np.diff(peaks)
    print(f"탐지된 피크 위치: {peaks}")
    print(f"주기 간격: {intervals}")
    print(f"평균 주기: {np.mean(intervals):.1f}일, 표준편차: {np.std(intervals):.1f}일")

김개발 씨는 계절성을 이해하고 나니 자신감이 붙었습니다. 그런데 경제 뉴스 데이터를 분석하다 보니 이상한 점을 발견했습니다.

경기가 좋아졌다 나빠졌다 반복되는데, 그 간격이 일정하지 않았습니다. "이것도 계절성인가요?" 김개발 씨가 물었습니다.

박시니어 씨가 고개를 저었습니다. "아니요, 그건 주기성이에요.

계절성과 비슷하지만 결정적인 차이가 있습니다." 그렇다면 주기성과 계절성은 어떻게 다를까요? 쉽게 비유하자면, 계절성은 지구의 공전과 같습니다.

지구는 정확히 365일마다 태양 주위를 한 바퀴 돕니다. 이 주기는 변하지 않습니다.

반면 주기성은 심장 박동과 같습니다. 쉬고 있을 때는 느리고, 운동할 때는 빨라집니다.

반복은 되지만 간격이 일정하지 않지요. 실무에서 이 구분이 왜 중요할까요?

계절성은 예측이 쉽습니다. "다음 12월에도 매출이 오를 것이다"라고 자신 있게 말할 수 있습니다.

하지만 주기성은 다릅니다. 경기 순환의 정확한 시점을 맞추기 어렵습니다.

"언젠가 불황이 올 것이다"라고는 말할 수 있지만, "3년 후 2월에 불황이 온다"라고 단언하기는 어렵습니다. 코드를 살펴보겠습니다.

**cycle_periods = [120, 180, 150, 200, 140]**은 각각 다른 길이의 주기를 나타냅니다. 실제 경기 순환처럼 매번 다른 간격으로 고점과 저점이 반복됩니다.

이것이 계절성과의 핵심 차이입니다. find_peaks 함수는 데이터에서 **고점(피크)**을 자동으로 탐지합니다.

distance=50은 최소 50일 간격으로 피크를 찾으라는 의미이고, prominence=10은 주변보다 10 이상 튀어나온 점만 피크로 인정한다는 뜻입니다. **np.diff(peaks)**는 연속된 피크 사이의 간격을 계산합니다.

계절성이라면 이 간격이 거의 일정해야 하지만, 주기성이라면 간격의 표준편차가 큽니다. 실제 현업에서는 어떻게 구분할까요?

소매업의 주간 매출 패턴은 계절성입니다. 매주 토요일에 매출이 높고, 화요일에 낮다면 이 패턴은 다음 주에도 반복될 것입니다.

반면 부동산 시장의 상승-하락 사이클은 주기성입니다. 상승장이 5년 갈 수도 있고, 3년 만에 끝날 수도 있습니다.

주의할 점이 있습니다. 주기성 분석에서 흔히 하는 실수는 충분하지 않은 데이터로 주기를 단정 짓는 것입니다.

2번의 고점만 관찰하고 "100일 주기다"라고 결론 내리면 안 됩니다. 최소 4-5번 이상의 반복을 관찰해야 대략적인 주기 범위를 추정할 수 있습니다.

김개발 씨가 여러 시계열 데이터에 find_peaks를 적용해봤습니다. 어떤 데이터는 간격이 거의 일정했고, 어떤 데이터는 들쑥날쑥했습니다.

"이제 계절성과 주기성을 구분할 수 있겠어요!" 박시니어 씨가 미소를 지었습니다. "잘했어요.

이제 이 모든 걸 합쳐서 실전에 적용해볼까요?"

실전 팁

💡 - 간격의 표준편차가 작으면 계절성, 크면 주기성으로 판단하세요

  • 주기성 분석에는 최소 4-5회 이상의 반복 데이터가 필요합니다
  • find_peaks의 distance와 prominence 파라미터를 데이터에 맞게 조절하세요

5. STL 분해로 완전한 분석하기

김개발 씨가 지금까지 배운 것을 정리했습니다. "추세, 계절성, 잔차...

이 세 가지를 한 번에 분리할 수 있는 더 좋은 방법은 없나요?" 박시니어 씨가 눈을 빛냈습니다. "있죠.

STL 분해라고, 업계 표준으로 쓰이는 강력한 기법이에요."

**STL(Seasonal and Trend decomposition using Loess)**은 시계열을 추세, 계절성, 잔차로 분해하는 강력한 알고리즘입니다. 기존의 seasonal_decompose보다 이상치에 강건하고, 계절성 패턴이 시간에 따라 변하는 경우도 처리할 수 있습니다.

마치 MRI가 신체를 여러 층으로 촬영하여 정밀 진단하듯, STL은 시계열 데이터를 정밀하게 분해합니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np
from statsmodels.tsa.seasonal import STL
import matplotlib.pyplot as plt

# 복잡한 패턴의 데이터 생성
np.random.seed(42)
days = 730
t = np.arange(days)

# 비선형 추세 + 변화하는 계절성 + 이상치
trend = 100 + 0.1 * t + 0.0001 * t**2
seasonality = (15 + 0.01 * t) * np.sin(2 * np.pi * t / 365)
noise = np.random.randn(days) * 5
# 이상치 추가
outliers = np.zeros(days)
outliers[[100, 400, 600]] = [50, -40, 60]

sales = trend + seasonality + noise + outliers

# STL 분해 수행
df = pd.Series(sales, index=pd.date_range('2022-01-01', periods=days))
stl = STL(df, seasonal=365, robust=True)
result = stl.fit()

# 각 성분 출력
print("추세 성분 (처음 5개):", result.trend[:5].values)
print("계절 성분 (처음 5개):", result.seasonal[:5].values)
print("잔차 성분 (처음 5개):", result.resid[:5].values)
print(f"\n잔차의 표준편차: {result.resid.std():.2f}")

김개발 씨는 seasonal_decompose로 분석을 잘 해왔습니다. 그런데 어느 날 이상한 데이터를 만났습니다.

특정 날짜에 매출이 갑자기 치솟았다가 다시 정상으로 돌아오는 이상치가 포함되어 있었습니다. seasonal_decompose를 적용하니 결과가 이상하게 나왔습니다.

"이 이상치 때문에 분해가 제대로 안 되는 것 같아요." 김개발 씨가 한숨을 쉬었습니다. 박시니어 씨가 다가왔습니다.

"그럴 때 쓰는 게 STL 분해예요. 이상치에 훨씬 강건합니다." STL이란 무엇일까요?

STL은 Seasonal and Trend decomposition using Loess의 약자입니다. Loess는 국소 회귀(Local Regression)라는 통계 기법인데, 데이터의 각 지점에서 주변 데이터만 활용해 부드러운 곡선을 그립니다.

마치 화가가 점묘법으로 그림을 그릴 때, 가까운 점들의 색을 섞어 부드러운 그라데이션을 만드는 것과 같습니다. 기존 seasonal_decompose와 무엇이 다를까요?

seasonal_decompose는 이상치에 민감합니다. 하나의 극단적인 값이 전체 분해 결과를 왜곡시킬 수 있습니다.

반면 STL의 robust=True 옵션은 이상치의 영향을 줄여줍니다. 또한 계절성 패턴이 시간에 따라 변하는 경우도 잘 처리합니다.

코드를 자세히 분석해보겠습니다. **(15 + 0.01 * t)**를 계절성 진폭에 곱한 부분에 주목하세요.

이것은 시간이 지남에 따라 계절 변동의 크기가 커지는 상황을 시뮬레이션합니다. 실제로 많은 비즈니스가 성장하면서 계절 변동의 절대적 크기도 함께 커집니다.

**outliers[[100, 400, 600]] = [50, -40, 60]**은 100일, 400일, 600일 차에 인위적인 이상치를 추가합니다. 프로모션, 시스템 장애, 특별 이벤트 등으로 발생하는 비정상적인 값을 모방한 것입니다.

**STL(df, seasonal=365, robust=True)**에서 robust=True가 핵심입니다. 이 옵션을 켜면 STL이 반복적으로 계산하면서 이상치를 탐지하고 그 영향을 줄입니다.

실제 현업에서의 활용 사례를 살펴봅시다. 전자상거래 회사에서 일별 매출을 분석한다고 가정해봅시다.

블랙프라이데이나 11번가 데이 같은 대규모 할인 행사 기간에는 매출이 평소의 5-10배가 됩니다. 이런 날을 그대로 두고 분석하면 결과가 왜곡됩니다.

STL의 robust 옵션을 사용하면 이런 특이일의 영향을 자동으로 줄여줍니다. 하지만 주의할 점도 있습니다.

seasonal 파라미터를 잘못 설정하면 문제가 됩니다. 주간 계절성을 분석하고 싶은데 seasonal=365로 설정하면 원하는 결과를 얻을 수 없습니다.

또한 robust=True는 계산 시간이 더 오래 걸리므로, 이상치가 별로 없는 깨끗한 데이터에서는 굳이 사용할 필요가 없습니다. 김개발 씨가 STL 분해 결과를 시각화해보니, 이상치가 있던 부분도 깔끔하게 처리되어 있었습니다.

"오, 이상치가 잔차에 잘 분리됐네요!" 박시니어 씨가 만족스럽게 고개를 끄덕입니다. "이제 예측까지 해볼까요?"

실전 팁

💡 - 이상치가 있는 데이터에서는 반드시 robust=True를 사용하세요

  • seasonal 파라미터는 분석하려는 계절성 주기와 일치시키세요
  • STL 결과의 잔차를 확인하여 모델이 데이터를 잘 설명하는지 검증하세요

6. 패턴 기반 예측 실전

드디어 마지막 단계입니다. 김개발 씨가 팀장님에게 분석 결과를 보고하자, 팀장님이 물었습니다.

"좋아요, 패턴은 알겠어요. 그래서 다음 달 매출은 얼마나 될 것 같아요?" 김개발 씨는 잠시 당황했지만, 배운 것을 총동원해 예측 모델을 만들기로 했습니다.

시계열 패턴 분석의 궁극적인 목표는 미래 예측입니다. 추세, 계절성, 주기성을 파악했다면 이를 바탕으로 앞으로의 값을 예측할 수 있습니다.

마치 일기예보가 과거의 기상 패턴을 분석해 내일 날씨를 예측하듯, 우리도 과거 데이터의 패턴으로 미래 매출을 예측합니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np
from statsmodels.tsa.holtwinters import ExponentialSmoothing

# 예측용 데이터 준비
np.random.seed(42)
days = 365 * 2
t = np.arange(days)
trend = 100 + 0.1 * t
seasonality = 20 * np.sin(2 * np.pi * t / 365)
noise = np.random.randn(days) * 5
sales = trend + seasonality + noise

# 시계열 데이터 생성
dates = pd.date_range('2022-01-01', periods=days, freq='D')
df = pd.Series(sales, index=dates)

# Holt-Winters 모델로 예측 (추세 + 계절성 반영)
model = ExponentialSmoothing(
    df,
    trend='add',           # 추세 성분 포함
    seasonal='add',        # 계절 성분 포함
    seasonal_periods=365   # 연간 계절성
)
fitted = model.fit()

# 향후 90일 예측
forecast = fitted.forecast(90)
print("향후 90일 예측값:")
print(forecast.head(10))
print(f"\n예측 기간: {forecast.index[0]} ~ {forecast.index[-1]}")
print(f"평균 예측 매출: {forecast.mean():.2f}")

김개발 씨는 지난 몇 주간 시계열 분석의 기초를 탄탄히 다졌습니다. 추세를 파악하고, 계절성을 분리하고, STL로 정밀 분해까지 할 수 있게 되었습니다.

이제 마지막 관문인 예측에 도전할 차례입니다. 팀장님의 질문은 간단했습니다.

"다음 달 매출 얼마나 될 것 같아요?" 하지만 이 간단한 질문에 답하려면 지금까지 배운 모든 것을 종합해야 합니다. 그렇다면 어떻게 예측할 수 있을까요?

쉽게 비유하자면, 시계열 예측은 운전 중 앞을 내다보는 것과 같습니다. 지금까지 달려온 길의 패턴(직선인지, 곡선인지, 오르막인지)을 보면 앞으로 길이 어떻게 이어질지 어느 정도 예측할 수 있습니다.

물론 갑작스러운 변화가 있을 수 있지만, 대체로 패턴은 이어집니다. Holt-Winters 지수평활법은 가장 널리 쓰이는 시계열 예측 기법 중 하나입니다.

이 방법은 세 가지 요소를 동시에 고려합니다. 레벨(현재 수준), 추세(변화 방향), **계절성(반복 패턴)**입니다.

각 요소에 가중치를 주어 예측하는데, 최근 데이터에 더 높은 가중치를 부여합니다. 이것이 "지수(Exponential)"라는 이름의 유래입니다.

코드를 단계별로 살펴보겠습니다. ExponentialSmoothing 클래스를 생성할 때 세 가지 핵심 파라미터를 설정합니다.

**trend='add'**는 덧셈 형태의 추세를 가정합니다. 매일 일정한 양만큼 증가(또는 감소)한다는 의미입니다.

**seasonal='add'**는 계절 변동도 덧셈 형태로 가정합니다. seasonal_periods=365는 연간 주기의 계절성을 설정합니다.

**model.fit()**은 과거 데이터를 학습하여 최적의 파라미터를 찾습니다. 이 과정에서 알고리즘은 추세의 기울기, 계절성의 패턴, 각 요소의 가중치를 자동으로 계산합니다.

**fitted.forecast(90)**은 학습된 모델로 향후 90일의 값을 예측합니다. 예측값은 pandas Series로 반환되어 바로 분석에 활용할 수 있습니다.

실제 현업에서는 어떻게 활용할까요? 재고 관리 시스템을 운영한다고 가정해봅시다.

다음 달에 어떤 상품이 얼마나 팔릴지 예측할 수 있다면, 적정 재고량을 미리 확보할 수 있습니다. 너무 많이 발주하면 재고 비용이 늘어나고, 너무 적게 발주하면 품절로 매출 기회를 잃습니다.

정확한 예측은 곧 비용 절감과 매출 극대화로 이어집니다. 주의할 점도 있습니다.

예측은 과거 패턴이 미래에도 지속된다고 가정합니다. 하지만 코로나19 팬데믹처럼 예상치 못한 사건이 발생하면 과거 패턴이 무너집니다.

따라서 예측 모델을 맹신하지 말고, 정기적으로 모델을 업데이트하고 예측 정확도를 모니터링해야 합니다. 또한 예측 구간이 길어질수록 불확실성이 커집니다.

내일 매출 예측은 비교적 정확하지만, 1년 후 매출 예측은 오차가 클 수밖에 없습니다. 예측 결과와 함께 신뢰 구간을 제시하는 것이 바람직합니다.

김개발 씨가 예측 결과를 그래프로 시각화하여 팀장님께 보고했습니다. "과거 패턴을 분석한 결과, 다음 달 예상 매출은 약 250억 원이며, 신뢰 구간은 230억~270억 원입니다." 팀장님이 만족스럽게 고개를 끄덕였습니다.

"좋아요, 이걸 기반으로 예산 계획을 세워봅시다." 박시니어 씨가 김개발 씨의 어깨를 두드렸습니다. "한 달 만에 많이 성장했네요.

이제 시계열 분석의 기본은 완전히 익힌 거예요!"

실전 팁

💡 - 예측 기간은 데이터 기간의 1/3을 넘지 않도록 하세요

  • 예측 결과와 실제 값을 비교하여 모델 정확도를 지속적으로 모니터링하세요
  • 중요한 의사결정에는 여러 모델의 예측을 앙상블하여 사용하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#TimeSeries#Trend#Seasonality#Forecasting#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.