시계열 분석 및 예측 마스터
시계열 데이터 분석의 모든 것을 마스터합니다. 기본 패턴 분석부터 ARIMA, SARIMA, Facebook Prophet, LSTM 딥러닝 모델까지 실무에서 사용되는 모든 예측 기법을 학습합니다. 판매량 예측, 트래픽 예측, 주가 예측 등 실전 프로젝트를 통해 MAE, RMSE, AIC 등 평가 지표를 활용한 모델 비교 방법을 익힙니다.
학습 항목
본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
이미지 로딩 중...
시계열 데이터 기초 및 특징 이해
시계열 데이터의 기본 개념부터 트렌드, 계절성, 노이즈 분석까지 초급 개발자가 알아야 할 핵심 내용을 실무 예제와 함께 설명합니다. 주식 가격, 센서 데이터, 웹 트래픽 등 실제 데이터를 다루는 방법을 배웁니다.
목차
- 시계열_데이터란_무엇인가
- 트렌드_분석하기
- 계절성_패턴_발견하기
- 노이즈_이해하고_처리하기
- 시계열_분해로_전체_그림_보기
- 리샘플링으로_시간_단위_변환하기
- 시차_상관관계_분석하기
- 이동_윈도우_통계로_변동성_추적하기
1. 시계열 데이터란 무엇인가
어느 날 김개발 씨가 회사에서 새로운 프로젝트를 맡게 되었습니다. "우리 쇼핑몰의 일별 매출 데이터를 분석해서 다음 달 매출을 예측해 주세요." 김개발 씨는 엑셀 파일을 열어보았습니다.
날짜와 숫자가 빼곡히 적혀 있는데, 도대체 어디서부터 시작해야 할지 막막했습니다.
시계열 데이터는 시간 순서대로 나열된 데이터를 말합니다. 마치 일기장에 매일매일 기록을 남기는 것처럼, 시간의 흐름에 따라 측정된 값들의 연속입니다.
주식 가격, 기온 변화, 웹사이트 방문자 수 등 우리 주변의 수많은 데이터가 시계열 형태로 존재합니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
# 시계열 데이터 생성하기
dates = pd.date_range(start='2024-01-01', periods=30, freq='D')
sales = [120, 135, 142, 128, 155, 148, 160, 145, 138, 152,
165, 172, 158, 149, 175, 182, 168, 155, 190, 185,
178, 165, 195, 188, 172, 168, 202, 195, 185, 210]
# DataFrame으로 변환
df = pd.DataFrame({'date': dates, 'sales': sales})
df.set_index('date', inplace=True)
# 기본 통계 확인
print(df.describe())
print(f"데이터 기간: {df.index.min()} ~ {df.index.max()}")
김개발 씨는 입사 6개월 차 데이터 분석가입니다. 지금까지는 단순한 집계 작업만 해왔는데, 이번에 처음으로 시계열 분석을 맡게 되었습니다.
선배인 박시니어 씨에게 조심스럽게 물어봤습니다. "선배님, 시계열 데이터가 정확히 뭔가요?
그냥 날짜가 있는 데이터 아닌가요?" 박시니어 씨가 미소를 지으며 답했습니다. "좋은 질문이에요.
시계열 데이터는 단순히 날짜가 있는 게 아니라, 시간의 순서가 의미를 가지는 데이터예요." 그렇다면 시계열 데이터란 정확히 무엇일까요? 쉽게 비유하자면, 시계열 데이터는 마치 성장 기록표와 같습니다.
아이의 키를 매달 측정해서 기록하면, 단순히 키 값들의 나열이 아니라 "성장"이라는 패턴을 발견할 수 있습니다. 3월에 측정한 키와 9월에 측정한 키는 순서를 바꿀 수 없습니다.
시간의 순서 자체가 정보를 담고 있기 때문입니다. 시계열 데이터가 특별한 이유는 무엇일까요?
일반적인 데이터는 각 행이 독립적입니다. 하지만 시계열 데이터에서는 이전 값이 다음 값에 영향을 미칩니다.
어제 주가가 10,000원이었다면, 오늘 주가가 100원이 되기는 어렵습니다. 이런 연속성이 시계열 데이터의 핵심입니다.
위의 코드를 살펴보겠습니다. 먼저 pd.date_range 함수로 날짜 인덱스를 생성합니다.
2024년 1월 1일부터 30일간의 날짜가 만들어집니다. 그 다음 각 날짜에 해당하는 매출 데이터를 리스트로 준비합니다.
핵심은 set_index 부분입니다. 날짜 컬럼을 인덱스로 설정하면, Pandas가 이 데이터를 시계열로 인식합니다.
이렇게 하면 날짜 기반의 슬라이싱, 리샘플링 등 강력한 기능을 사용할 수 있게 됩니다. 실제 현업에서는 어떤 시계열 데이터를 다룰까요?
금융 분야에서는 주가, 환율, 거래량을 분석합니다. 제조업에서는 센서 데이터와 생산량을 모니터링합니다.
IT 분야에서는 서버 로그, API 호출량, 사용자 트래픽을 추적합니다. 모두 시간의 흐름에 따른 패턴을 파악하는 것이 목표입니다.
주의할 점도 있습니다. 시계열 데이터를 다룰 때 가장 흔한 실수는 결측치 처리입니다.
일반 데이터에서는 결측치가 있는 행을 삭제해도 되지만, 시계열에서는 시간의 연속성이 깨집니다. 따라서 보간이나 전방 채움 같은 방법을 사용해야 합니다.
김개발 씨는 고개를 끄덕였습니다. "아, 그래서 날짜가 빠진 부분을 그냥 지우면 안 되는 거군요!"
실전 팁
💡 - 시계열 데이터는 반드시 날짜/시간 컬럼을 인덱스로 설정하세요
- 데이터를 받으면 먼저 시간 순서대로 정렬되어 있는지 확인하세요
- 결측치가 있다면 삭제보다 보간을 고려하세요
2. 트렌드 분석하기
김개발 씨가 매출 데이터를 그래프로 그려보았습니다. 위아래로 출렁거리는 선이 보입니다.
"이게 오르는 건가요, 내리는 건가요?" 박시니어 씨가 다가와서 말했습니다. "그건 트렌드를 봐야 알 수 있어요.
눈에 보이는 변동을 걷어내고 큰 흐름을 파악하는 거죠."
트렌드는 시계열 데이터에서 장기적인 증가 또는 감소 경향을 말합니다. 마치 주식 차트에서 잔물결은 무시하고 큰 파도의 방향만 보는 것과 같습니다.
이동평균을 사용하면 단기 변동을 부드럽게 만들어 트렌드를 쉽게 파악할 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
import matplotlib.pyplot as plt
# 샘플 데이터 생성
dates = pd.date_range(start='2024-01-01', periods=90, freq='D')
np.random.seed(42)
base_trend = np.linspace(100, 200, 90) # 상승 트렌드
noise = np.random.normal(0, 15, 90) # 랜덤 노이즈
sales = base_trend + noise
df = pd.DataFrame({'sales': sales}, index=dates)
# 이동평균으로 트렌드 추출
df['MA_7'] = df['sales'].rolling(window=7).mean() # 7일 이동평균
df['MA_30'] = df['sales'].rolling(window=30).mean() # 30일 이동평균
# 트렌드 시각화
print(f"시작점 평균: {df['sales'][:7].mean():.1f}")
print(f"종료점 평균: {df['sales'][-7:].mean():.1f}")
김개발 씨는 매출 그래프를 뚫어지게 바라보았습니다. 어떤 날은 올랐다가, 어떤 날은 떨어졌다가, 도무지 패턴을 찾기가 어려웠습니다.
박시니어 씨가 조언했습니다. "나무만 보지 말고 숲을 봐야 해요.
트렌드를 찾아보세요." 트렌드란 무엇일까요? 쉽게 비유하자면, 트렌드는 마치 에스컬레이터와 같습니다.
에스컬레이터를 타고 올라가는 사람이 제자리에서 발을 동동 굴러도, 결국 위로 올라가고 있습니다. 발의 움직임은 단기 변동이고, 에스컬레이터의 방향이 트렌드입니다.
왜 트렌드를 파악해야 할까요? 투자자가 주식을 살 때, 오늘 1% 올랐다고 좋아하고 내일 1% 떨어졌다고 슬퍼하면 제대로 된 판단을 할 수 없습니다.
중요한 것은 전체적인 방향입니다. 사업도 마찬가지입니다.
일별 매출의 등락에 일희일비하지 않고, 월별, 분기별 성장 추이를 봐야 합니다. 이동평균이란 무엇인지 코드로 살펴보겠습니다.
**rolling(window=7).mean()**은 최근 7일간의 평균을 계산합니다. 매일 새로운 값이 들어오면 가장 오래된 값은 빠지고 새 값이 포함되어 평균이 계산됩니다.
이것이 "이동"평균인 이유입니다. 윈도우 크기에 따라 결과가 달라집니다.
7일 이동평균은 주간 변동을 부드럽게 만들고, 30일 이동평균은 월간 트렌드를 보여줍니다. 윈도우가 클수록 더 부드러운 곡선이 나오지만, 최신 변화에 둔감해집니다.
실무에서는 어떻게 활용할까요? 주식 분석에서는 5일선, 20일선, 60일선을 많이 사용합니다.
단기 이동평균이 장기 이동평균을 위로 뚫고 올라가면 골든크로스라고 하여 상승 신호로 해석합니다. 반대는 데드크로스입니다.
웹 서비스에서는 일별 활성 사용자 수의 7일 이동평균을 모니터링합니다. 하루 이틀의 변동에 과민 반응하지 않으면서도 성장 추이를 파악할 수 있습니다.
주의할 점이 있습니다. 이동평균은 후행 지표입니다.
과거 데이터를 기반으로 계산하기 때문에, 급격한 변화가 있을 때 즉시 반영되지 않습니다. 코로나19 팬데믹 초기처럼 갑작스러운 변화가 있을 때는 이동평균만 보면 늦을 수 있습니다.
김개발 씨가 30일 이동평균선을 그려보니, 확실히 우상향하는 것이 보였습니다. "아, 매출이 꾸준히 성장하고 있었네요!"
실전 팁
💡 - 주간 패턴이 강한 데이터는 7일 이동평균을 사용하세요
- 이동평균 초기 구간에는 NaN이 발생하므로 주의하세요
- 여러 윈도우 크기를 비교해보며 적절한 값을 찾으세요
3. 계절성 패턴 발견하기
김개발 씨가 1년치 데이터를 분석하다가 이상한 점을 발견했습니다. "왜 매번 12월에 매출이 급등하고, 2월에는 떨어지죠?" 박시니어 씨가 웃으며 답했습니다.
"그건 계절성이에요. 크리스마스 시즌에 사람들이 선물을 많이 사니까요.
이 패턴을 알면 재고 준비를 미리 할 수 있어요."
계절성은 일정한 주기로 반복되는 패턴을 말합니다. 연간 계절성(여름 에어컨 판매), 월간 계절성(월급날 소비 증가), 주간 계절성(주말 트래픽 감소) 등 다양한 주기가 있습니다.
계절성을 파악하면 미래 예측의 정확도를 크게 높일 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
# 1년치 일별 데이터 생성 (계절성 포함)
dates = pd.date_range(start='2024-01-01', periods=365, freq='D')
np.random.seed(42)
# 계절성 패턴 생성: 여름에 높고 겨울에 낮은 패턴
day_of_year = np.arange(365)
seasonal = 30 * np.sin(2 * np.pi * day_of_year / 365) # 연간 주기
weekly = 10 * np.sin(2 * np.pi * day_of_year / 7) # 주간 주기
trend = np.linspace(0, 20, 365) # 완만한 상승
noise = np.random.normal(0, 5, 365)
sales = 100 + trend + seasonal + weekly + noise
df = pd.DataFrame({'sales': sales}, index=dates)
# 요일별 평균 분석
df['weekday'] = df.index.day_name()
weekday_avg = df.groupby('weekday')['sales'].mean()
print("요일별 평균 매출:")
print(weekday_avg.round(1))
김개발 씨는 매출 데이터가 파도처럼 출렁거리는 것을 보았습니다. 그런데 자세히 보니, 그 파도에 규칙이 있는 것 같았습니다.
"선배님, 이 패턴이 매년 비슷하게 반복되는 것 같아요." 박시니어 씨가 고개를 끄덕였습니다. "잘 봤어요.
그게 바로 계절성이에요." 계절성이란 무엇일까요? 쉽게 비유하자면, 계절성은 마치 심장 박동과 같습니다.
심장은 규칙적인 리듬으로 뛰고, 그 리듬은 예측 가능합니다. 데이터에도 이런 리듬이 있습니다.
아이스크림 가게는 여름에 매출이 오르고 겨울에 떨어집니다. 이 패턴은 매년 반복됩니다.
계절성에는 여러 종류가 있습니다. 연간 계절성은 1년 주기로 반복됩니다.
스키 리조트는 겨울에, 해수욕장은 여름에 성수기를 맞습니다. 월간 계절성은 월급날에 소비가 증가하는 패턴입니다.
주간 계절성은 주말과 평일의 차이입니다. 음식 배달 앱은 금요일 저녁에 주문이 폭주합니다.
코드를 살펴보겠습니다. np.sin 함수로 주기적인 패턴을 만들었습니다.
sin 함수는 -1과 1 사이를 오가는 파동을 그립니다. 주기를 365로 나누면 1년을 한 주기로 하는 패턴이 됩니다.
7로 나누면 일주일 주기가 됩니다. groupby를 사용해 요일별 평균을 계산했습니다.
이렇게 하면 어떤 요일에 매출이 높은지 한눈에 파악할 수 있습니다. 실제 데이터에서는 이런 분석으로 인력 배치나 마케팅 타이밍을 결정합니다.
실무에서는 어떻게 활용할까요? 전자상거래 회사는 블랙프라이데이, 크리스마스 시즌의 트래픽 급증을 대비해 서버를 증설합니다.
계절성을 무시하면 서버가 다운되는 대참사가 발생할 수 있습니다. 콜센터는 월초에 상담사를 더 배치합니다.
월급을 받은 사람들이 문의 전화를 많이 하기 때문입니다. 주의해야 할 점이 있습니다.
계절성을 가짜 계절성과 혼동하면 안 됩니다. 데이터가 충분하지 않으면 우연의 일치를 계절성으로 착각할 수 있습니다.
최소 2-3년 이상의 데이터로 패턴이 반복되는지 확인해야 합니다. 또한 계절성은 변할 수 있습니다.
코로나19 이후 재택근무가 늘면서 평일과 주말의 차이가 줄어든 서비스도 있습니다. 과거 패턴을 맹신하면 안 됩니다.
김개발 씨는 월별로 데이터를 나눠 그래프를 그려보았습니다. "정말이네요, 매년 비슷한 패턴이 보여요!"
실전 팁
💡 - 계절성 분석에는 최소 2년 이상의 데이터가 필요합니다
- 여러 주기의 계절성이 중첩되어 있을 수 있으니 각각 분리해서 분석하세요
- 휴일이나 특수 이벤트는 계절성과 별도로 처리하세요
4. 노이즈 이해하고 처리하기
김개발 씨가 예측 모델을 만들었는데, 실제 값과 차이가 많이 났습니다. "트렌드도 맞추고, 계절성도 반영했는데 왜 이렇게 오차가 크죠?" 박시니어 씨가 그래프를 가리켰습니다.
"저 들쭉날쭉한 부분이 보이죠? 그게 노이즈예요.
완전히 예측하기는 어렵지만, 이해하고 관리할 수는 있어요."
노이즈는 트렌드와 계절성으로 설명되지 않는 불규칙한 변동입니다. 마치 라디오의 잡음처럼, 신호에 섞여 들어오는 무작위적인 요소입니다.
노이즈를 완전히 제거할 수는 없지만, 그 크기를 파악하고 적절히 처리하면 더 나은 분석이 가능합니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
# 데이터 생성
dates = pd.date_range(start='2024-01-01', periods=100, freq='D')
np.random.seed(42)
trend = np.linspace(100, 150, 100)
seasonal = 10 * np.sin(2 * np.pi * np.arange(100) / 30)
noise = np.random.normal(0, 8, 100) # 표준편차 8인 노이즈
sales = trend + seasonal + noise
df = pd.DataFrame({'sales': sales}, index=dates)
# 노이즈 추정: 이동평균과의 차이
df['smoothed'] = df['sales'].rolling(window=7, center=True).mean()
df['residual'] = df['sales'] - df['smoothed']
# 노이즈 크기 분석
noise_std = df['residual'].std()
print(f"노이즈 표준편차: {noise_std:.2f}")
print(f"신호 대비 노이즈 비율: {noise_std / df['sales'].mean() * 100:.1f}%")
# 이상치 탐지 (2 표준편차 초과)
outliers = df[abs(df['residual']) > 2 * noise_std]
print(f"이상치 개수: {len(outliers)}")
김개발 씨는 좌절감을 느꼈습니다. 분명히 트렌드도 반영하고, 계절성도 고려했는데 예측값과 실제값의 차이가 여전히 컸습니다.
박시니어 씨가 위로했습니다. "100% 정확한 예측은 불가능해요.
중요한 건 노이즈를 이해하는 거예요." 노이즈란 무엇일까요? 쉽게 비유하자면, 노이즈는 마치 바람에 흔들리는 나뭇잎과 같습니다.
나무는 계절에 따라 성장하고(트렌드), 봄에 새잎이 나고 가을에 떨어집니다(계절성). 하지만 바람이 불면 나뭇잎은 예측할 수 없이 흔들립니다.
이 흔들림이 노이즈입니다. 노이즈는 왜 발생할까요?
수많은 작은 요인들이 노이즈를 만듭니다. 쇼핑몰 매출을 예로 들면, 경쟁사의 갑작스러운 할인 행사, 인플루언서의 우연한 언급, 날씨 변화, 뉴스 기사 등 무수히 많은 요소가 영향을 미칩니다.
이 모든 것을 예측하기는 불가능합니다. 코드를 살펴보겠습니다.
먼저 이동평균으로 데이터를 부드럽게 만듭니다. center=True 옵션은 현재 시점을 중심으로 앞뒤 데이터를 사용해 더 정확한 평활화를 합니다.
원본 데이터에서 평활화된 값을 빼면 **잔차(residual)**가 나오는데, 이것이 노이즈입니다. 노이즈의 표준편차를 계산하면 변동의 크기를 수치화할 수 있습니다.
예를 들어 표준편차가 8이면, 대부분의 노이즈가 -16에서 +16 사이에 있다는 의미입니다(2 표준편차 범위). 실무에서는 어떻게 활용할까요?
이상치 탐지가 대표적입니다. 노이즈의 정상 범위를 벗어나는 값은 특별한 원인이 있을 가능성이 높습니다.
서버 모니터링에서 평소와 다른 트래픽 패턴이 감지되면, 그것이 공격인지 바이럴 마케팅 성공인지 조사해야 합니다. 예측 모델의 신뢰 구간 설정에도 사용합니다.
"내일 매출은 100만 원"이라고 말하는 것보다 "내일 매출은 95% 확률로 80만 원에서 120만 원 사이"라고 말하는 것이 더 정직하고 유용합니다. 주의할 점이 있습니다.
노이즈와 신호를 구분하는 것이 중요합니다. 너무 적극적으로 노이즈를 제거하면 의미 있는 신호까지 사라질 수 있습니다.
반대로 노이즈를 신호로 착각하면 존재하지 않는 패턴을 추적하게 됩니다. 김개발 씨가 물었습니다.
"그러면 노이즈가 작을수록 좋은 건가요?" 박시니어 씨가 답했습니다. "꼭 그렇지는 않아요.
노이즈 자체보다 신호 대비 노이즈 비율이 중요해요. 매출이 1억 원인데 노이즈가 100만 원이면 1%지만, 매출이 1000만 원인데 노이즈가 100만 원이면 10%니까요."
실전 팁
💡 - 노이즈 크기를 파악해 예측의 불확실성을 정량화하세요
- 2 표준편차를 벗어나는 값은 이상치로 의심하고 원인을 조사하세요
- 노이즈를 제거하려 하기보다 이해하고 관리하는 접근이 중요합니다
5. 시계열 분해로 전체 그림 보기
김개발 씨가 지금까지 배운 내용을 정리해보았습니다. "트렌드, 계절성, 노이즈...
각각은 이해했는데, 이걸 한 번에 분리할 수 있는 방법은 없나요?" 박시니어 씨가 화면에 코드를 띄웠습니다. "있죠.
시계열 분해라는 기법이에요. 마치 음악에서 각 악기 소리를 분리하는 것처럼, 시계열의 구성 요소를 분리할 수 있어요."
시계열 분해는 데이터를 트렌드, 계절성, 잔차로 분리하는 기법입니다. 마치 프리즘이 빛을 여러 색으로 분해하듯, 복잡한 시계열을 단순한 구성 요소로 나눕니다.
statsmodels 라이브러리의 seasonal_decompose 함수를 사용하면 손쉽게 분해할 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
from statsmodels.tsa.seasonal import seasonal_decompose
# 2년치 월별 데이터 생성
dates = pd.date_range(start='2022-01-01', periods=24, freq='M')
np.random.seed(42)
trend = np.linspace(100, 160, 24) # 상승 트렌드
seasonal = 20 * np.array([1, 0.5, 0, -0.5, -1, -0.8, -0.3, 0.2, 0.7, 1, 0.8, 0.3] * 2)
noise = np.random.normal(0, 3, 24)
sales = trend + seasonal + noise
df = pd.DataFrame({'sales': sales}, index=dates)
# 시계열 분해 수행
result = seasonal_decompose(df['sales'], model='additive', period=12)
# 각 구성요소 확인
print("원본 데이터 범위:", df['sales'].min().round(1), "~", df['sales'].max().round(1))
print("트렌드 범위:", result.trend.dropna().min().round(1), "~", result.trend.dropna().max().round(1))
print("계절성 범위:", result.seasonal.min().round(1), "~", result.seasonal.max().round(1))
print("잔차 표준편차:", result.resid.dropna().std().round(2))
김개발 씨는 지금까지 배운 개념들이 머릿속에서 뒤섞여 있었습니다. 트렌드를 따로 보고, 계절성을 따로 보고, 노이즈를 따로 봤는데 이것들이 어떻게 연결되는지 명확하지 않았습니다.
박시니어 씨가 새로운 도구를 소개했습니다. "시계열 분해를 사용하면 한 번에 모든 구성 요소를 볼 수 있어요." 시계열 분해란 무엇일까요?
쉽게 비유하자면, 시계열 분해는 마치 요리 레시피 역추적과 같습니다. 완성된 요리를 먹어보고, 어떤 재료가 얼마나 들어갔는지 알아내는 것입니다.
짠맛은 소금에서, 단맛은 설탕에서, 감칠맛은 MSG에서 왔다는 것을 분리해내는 것이죠. 왜 분해가 필요할까요?
각 구성 요소를 분리하면 더 정확한 분석이 가능합니다. 트렌드만 보면 사업이 성장하고 있는지 알 수 있고, 계절성만 보면 언제 준비해야 하는지 알 수 있고, 잔차만 보면 예측의 한계가 어느 정도인지 알 수 있습니다.
코드를 살펴보겠습니다. seasonal_decompose 함수가 핵심입니다.
model 파라미터는 additive(덧셈 모델)와 multiplicative(곱셈 모델) 중 선택합니다. 덧셈 모델은 원본 = 트렌드 + 계절성 + 잔차이고, 곱셈 모델은 원본 = 트렌드 x 계절성 x 잔차입니다.
period 파라미터는 계절성의 주기를 지정합니다. 월별 데이터에서 연간 계절성을 보려면 12를, 일별 데이터에서 주간 계절성을 보려면 7을 입력합니다.
덧셈 모델과 곱셈 모델은 언제 사용할까요? 계절 변동의 크기가 일정하면 덧셈 모델을 사용합니다.
예를 들어 겨울에 항상 10만 원 정도 매출이 떨어진다면 덧셈 모델이 적합합니다. 반면 계절 변동이 비율로 나타나면 곱셈 모델을 사용합니다.
겨울에 매출이 20% 감소한다면, 기본 매출이 100만 원일 때는 20만 원, 1000만 원일 때는 200만 원이 줄어듭니다. 실무에서는 어떻게 활용할까요?
분해 결과를 보면 여러 인사이트를 얻을 수 있습니다. 트렌드가 최근 꺾이기 시작했다면 원인을 조사해야 합니다.
계절성 패턴이 작년과 달라졌다면 시장 변화가 있을 수 있습니다. 잔차에서 특정 시점에 큰 값이 발견되면 그날 무슨 일이 있었는지 확인해야 합니다.
주의할 점이 있습니다. 분해 결과의 양 끝에는 NaN이 발생합니다.
이동평균 계산 시 양쪽 데이터가 부족하기 때문입니다. 또한 분해는 데이터가 충분히 있어야 의미가 있습니다.
최소한 2주기 이상의 데이터가 필요합니다. 연간 계절성을 보려면 2년 이상의 데이터가 있어야 합니다.
김개발 씨가 분해 결과 그래프를 보며 감탄했습니다. "와, 정말 깔끔하게 분리되네요!
이제 뭐가 뭔지 한눈에 보여요."
실전 팁
💡 - 데이터의 변동 패턴을 보고 additive와 multiplicative 중 적절한 모델을 선택하세요
- 분해 전에 결측치를 처리해야 합니다
- 분해 결과를 시각화하면 각 구성 요소를 직관적으로 파악할 수 있습니다
6. 리샘플링으로 시간 단위 변환하기
김개발 씨가 일별 매출 데이터를 가지고 월간 보고서를 작성해야 했습니다. 엑셀에서 일일이 월별로 합계를 구하려니 시간이 오래 걸렸습니다.
"이거 자동화할 수 없나요?" 박시니어 씨가 Pandas를 열었습니다. "리샘플링을 사용하면 한 줄로 끝나요.
시간 단위를 자유자재로 바꿀 수 있거든요."
리샘플링은 시계열 데이터의 시간 간격을 변환하는 기법입니다. 일별 데이터를 주별 또는 월별로 집계하거나, 반대로 월별 데이터를 일별로 보간할 수 있습니다.
Pandas의 resample 메서드를 사용하면 간단하게 시간 단위를 변환할 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
# 일별 데이터 생성
dates = pd.date_range(start='2024-01-01', periods=90, freq='D')
np.random.seed(42)
daily_sales = np.random.randint(80, 150, 90)
df = pd.DataFrame({'sales': daily_sales}, index=dates)
# 다운샘플링: 일별 -> 주별, 월별
weekly = df.resample('W').sum() # 주별 합계
monthly = df.resample('M').agg({
'sales': ['sum', 'mean', 'std'] # 월별 합계, 평균, 표준편차
})
print("원본 (일별) 데이터 수:", len(df))
print("주별 집계 후 데이터 수:", len(weekly))
print("월별 집계 후 데이터 수:", len(monthly))
# 업샘플링: 일별 -> 시간별 (보간)
hourly = df.resample('H').interpolate(method='linear')
print("시간별 보간 후 데이터 수:", len(hourly))
김개발 씨는 엑셀에서 피벗 테이블을 만들다가 지쳐있었습니다. 90일치 데이터를 월별로 합산하는 단순한 작업인데, 왜 이렇게 번거로운 걸까요.
박시니어 씨가 다가와 물었습니다. "뭐 하고 있어요?" "일별 데이터를 월별로 바꾸고 있어요." "그거 Pandas로 하면 한 줄이에요." 리샘플링이란 무엇일까요?
쉽게 비유하자면, 리샘플링은 마치 지도의 축척 변경과 같습니다. 1:1000 지도에서는 골목길까지 보이지만, 1:100000 지도에서는 주요 도로만 보입니다.
같은 지역이지만 보는 관점(해상도)이 다른 것입니다. 리샘플링도 마찬가지로 데이터의 시간 해상도를 바꿉니다.
다운샘플링은 해상도를 낮추는 것입니다. 일별 데이터를 월별로 합산하면 디테일은 줄지만 전체 흐름이 명확해집니다.
경영진 보고서에는 일별 데이터보다 월별 요약이 더 적합합니다. 업샘플링은 해상도를 높이는 것입니다.
월별 데이터밖에 없는데 일별 추정치가 필요할 때 사용합니다. 물론 없는 데이터를 만들어내는 것이므로 보간 방법이 중요합니다.
코드를 살펴보겠습니다. **resample('W')**는 주간(Weekly) 단위로 리샘플링합니다.
'M'은 월간, 'Q'는 분기, 'Y'는 연간입니다. 리샘플링 후에는 집계 함수를 적용해야 합니다.
**sum()**은 합계, **mean()**은 평균, **max()**는 최댓값을 계산합니다. agg 메서드를 사용하면 여러 집계 함수를 동시에 적용할 수 있습니다.
월별 합계와 평균, 표준편차를 한 번에 계산하는 것이 가능합니다. 업샘플링에서는 interpolate 메서드로 빈 값을 채웁니다.
linear 방식은 두 점 사이를 직선으로 연결하고, spline 방식은 부드러운 곡선으로 연결합니다. 실무에서는 어떻게 활용할까요?
센서 데이터는 보통 초 단위나 밀리초 단위로 수집됩니다. 이 데이터를 분 단위나 시간 단위로 다운샘플링하면 분석이 훨씬 수월해집니다.
저장 공간도 절약됩니다. 반대로 일별 재고 데이터만 있는데 시간별 재고 추이가 궁금할 때는 업샘플링과 보간을 사용합니다.
단, 보간된 값은 추정치일 뿐 실제 값이 아니라는 점을 기억해야 합니다. 주의할 점이 있습니다.
다운샘플링할 때 정보 손실이 발생합니다. 일별 데이터를 월별 합계로 바꾸면, 월 초에 매출이 집중됐는지 월 말에 집중됐는지 알 수 없습니다.
목적에 맞는 집계 방법을 선택해야 합니다. 업샘플링은 더 주의가 필요합니다.
보간은 어디까지나 추정입니다. 선형 보간은 급격한 변화를 포착하지 못합니다.
보간된 데이터로 중요한 의사결정을 하면 안 됩니다. 김개발 씨는 감탄했습니다.
"정말 한 줄이네요! 엑셀에서 30분 걸리던 작업을..."
실전 팁
💡 - 다운샘플링 시 sum, mean, max 등 목적에 맞는 집계 함수를 선택하세요
- 업샘플링은 추정치를 생성하는 것이므로 신중하게 사용하세요
- 리샘플링 주기 코드: W(주), M(월), Q(분기), Y(연), H(시간), T(분), S(초)
7. 시차 상관관계 분석하기
김개발 씨가 마케팅팀에서 요청을 받았습니다. "광고를 집행하면 매출이 얼마나 오르나요?" 김개발 씨가 같은 날짜의 광고비와 매출을 비교했더니 상관관계가 약했습니다.
"이상하다..." 박시니어 씨가 힌트를 주었습니다. "광고 효과는 바로 나타나지 않아요.
시차를 두고 봐야 해요."
시차 상관관계는 두 시계열 사이의 지연된 관계를 분석하는 기법입니다. 광고는 집행 후 며칠 뒤에 매출로 연결되고, 금리 인상은 몇 개월 뒤에 소비에 영향을 미칩니다.
시차를 고려하면 숨겨진 인과관계를 발견할 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
# 광고비와 매출 데이터 생성 (매출은 광고 3일 후에 반응)
np.random.seed(42)
dates = pd.date_range(start='2024-01-01', periods=60, freq='D')
ad_spend = np.random.randint(50, 150, 60)
# 매출은 3일 전 광고비에 영향을 받음
base_sales = 500
sales = base_sales + np.roll(ad_spend, 3) * 2 + np.random.normal(0, 30, 60)
sales[:3] = base_sales + np.random.normal(0, 30, 3) # 처음 3일은 별도 처리
df = pd.DataFrame({
'ad_spend': ad_spend,
'sales': sales
}, index=dates)
# 시차별 상관관계 계산
print("시차별 상관관계 (광고비 -> 매출):")
for lag in range(0, 8):
correlation = df['ad_spend'].corr(df['sales'].shift(-lag))
print(f" {lag}일 후: {correlation:.3f}")
# 최적 시차 찾기
correlations = [df['ad_spend'].corr(df['sales'].shift(-lag)) for lag in range(8)]
best_lag = correlations.index(max(correlations))
print(f"\n최적 시차: {best_lag}일 (상관계수: {max(correlations):.3f})")
마케팅팀에서 매일 같은 질문이 들어왔습니다. "광고 효과가 있긴 한 건가요?" 김개발 씨는 광고비와 매출의 상관관계를 계산했는데, 0.1도 안 되는 약한 상관관계가 나왔습니다.
"광고를 해도 매출이 안 오른다는 건가요?" 김개발 씨는 혼란스러웠습니다. 박시니어 씨가 질문했습니다.
"광고를 보고 바로 구매하는 사람이 많을까요?" 시차 상관관계란 무엇일까요? 쉽게 비유하자면, 시차 상관관계는 마치 메아리와 같습니다.
산에서 소리를 지르면 몇 초 뒤에 메아리가 돌아옵니다. 원인(소리)과 결과(메아리)는 분명히 연결되어 있지만, 시간 차이가 있습니다.
이 차이를 고려하지 않으면 관계를 발견할 수 없습니다. 왜 시차가 발생할까요?
광고를 예로 들면, 고객이 광고를 보고 관심을 가진 뒤 비교하고, 고민하고, 결정하고, 구매합니다. 이 과정에 며칠이 걸립니다.
고관여 제품(자동차, 가전제품)은 더 오래 걸리고, 저관여 제품(음료, 과자)은 짧게 걸립니다. 코드를 살펴보겠습니다.
shift 메서드가 핵심입니다. **shift(-3)**은 데이터를 3칸 앞으로 당깁니다.
즉, 3일 후의 매출을 현재 광고비와 비교하게 됩니다. 여러 시차에 대해 상관관계를 계산하면 어느 시차에서 관계가 가장 강한지 알 수 있습니다.
위 코드에서는 0일부터 7일까지의 시차를 검토했습니다. 결과를 보면 3일 시차에서 상관관계가 가장 높게 나옵니다.
이는 광고 효과가 평균 3일 후에 나타난다는 의미입니다. 실무에서는 어떻게 활용할까요?
마케팅에서는 광고 효과 측정에 사용합니다. TV 광고, 온라인 광고, 인플루언서 마케팅 등 채널별로 효과가 나타나는 시점이 다릅니다.
이를 파악하면 예산 배분을 최적화할 수 있습니다. 경제 분석에서는 선행지표 발굴에 활용합니다.
기업의 채용 공고 수가 3개월 후 실업률과 상관관계가 있다면, 채용 공고를 보고 미래 고용 시장을 예측할 수 있습니다. 주의할 점이 있습니다.
상관관계는 인과관계가 아닙니다. 시차 상관관계가 높다고 해서 A가 B의 원인이라고 단정할 수 없습니다.
두 변수 모두에 영향을 미치는 제3의 변수가 있을 수 있습니다. 광고비와 매출이 모두 계절성의 영향을 받는다면, 둘 사이에 상관관계가 나타나도 직접적인 인과관계가 아닐 수 있습니다.
김개발 씨가 3일 시차를 적용해 다시 분석했습니다. 상관관계가 0.7로 높게 나왔습니다.
"광고 효과가 있긴 했네요! 3일 후에 나타나는 거였어요."
실전 팁
💡 - 여러 시차에 대해 상관관계를 계산하고 그래프로 시각화하세요
- 상관관계가 높은 시차가 발견되어도 인과관계를 섣불리 주장하지 마세요
- 계절성을 제거한 후 시차 분석을 하면 더 정확한 결과를 얻을 수 있습니다
8. 이동 윈도우 통계로 변동성 추적하기
김개발 씨가 주가 데이터를 분석하던 중, 투자팀에서 질문이 왔습니다. "요즘 변동성이 커진 것 같은데, 정량적으로 보여줄 수 있나요?" 김개발 씨는 전체 기간의 표준편차를 계산했지만, 그건 하나의 숫자일 뿐이었습니다.
"시간에 따라 변동성이 어떻게 변하는지 보고 싶어요."
이동 윈도우 통계는 일정 기간 단위로 통계량을 계산해 시간에 따른 변화를 추적하는 기법입니다. 이동평균뿐 아니라 이동표준편차, 이동최댓값, 이동최솟값 등 다양한 통계량을 계산할 수 있습니다.
이를 통해 변동성의 변화, 극단값의 발생 등을 모니터링할 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
# 주가 데이터 생성 (변동성이 증가하는 패턴)
np.random.seed(42)
dates = pd.date_range(start='2024-01-01', periods=120, freq='D')
# 전반부는 낮은 변동성, 후반부는 높은 변동성
volatility = np.concatenate([np.ones(60) * 2, np.ones(60) * 8])
returns = np.random.normal(0.001, volatility / 100)
price = 100 * np.cumprod(1 + returns)
df = pd.DataFrame({'price': price}, index=dates)
# 이동 윈도우 통계
window = 20
df['MA_20'] = df['price'].rolling(window).mean() # 이동평균
df['std_20'] = df['price'].rolling(window).std() # 이동표준편차
df['max_20'] = df['price'].rolling(window).max() # 이동최댓값
df['min_20'] = df['price'].rolling(window).min() # 이동최솟값
# 볼린저 밴드 계산
df['upper_band'] = df['MA_20'] + 2 * df['std_20']
df['lower_band'] = df['MA_20'] - 2 * df['std_20']
# 변동성 변화 확인
print("전반부 평균 변동성:", df['std_20'][30:60].mean().round(2))
print("후반부 평균 변동성:", df['std_20'][90:120].mean().round(2))
투자팀의 질문은 간단했지만, 답하기는 쉽지 않았습니다. "변동성"이란 무엇이고, 어떻게 측정해야 할까요?
박시니어 씨가 설명했습니다. "변동성은 보통 표준편차로 측정해요.
그런데 시장 상황은 계속 바뀌니까, 고정된 하나의 숫자가 아니라 시간에 따른 변화를 봐야 해요." 이동 윈도우 통계란 무엇일까요? 쉽게 비유하자면, 이동 윈도우 통계는 마치 차창 밖 풍경과 같습니다.
기차를 타고 가면서 창문으로 밖을 보면, 창문의 크기(윈도우)만큼의 풍경만 보입니다. 기차가 움직이면 보이는 풍경도 바뀝니다.
같은 방식으로, 시간의 창을 움직이면서 그 안의 데이터를 분석합니다. 왜 이동 윈도우가 필요할까요?
전체 기간의 표준편차는 하나의 숫자입니다. 120일 동안 변동성이 일정했는지, 중간에 급변했는지 알 수 없습니다.
하지만 20일 이동표준편차를 계산하면, 매일 "최근 20일간의 변동성"을 얻을 수 있고, 이 값의 변화를 추적할 수 있습니다. 코드를 살펴보겠습니다.
**rolling(window=20)**은 최근 20일간의 데이터로 계산하라는 의미입니다. 뒤에 붙는 메서드에 따라 평균(mean), 표준편차(std), 최댓값(max), 최솟값(min) 등을 계산합니다.
볼린저 밴드는 유명한 기술적 분석 도구입니다. 이동평균을 중심선으로 하고, 위아래로 2 표준편차만큼 떨어진 선을 그립니다.
가격이 상단 밴드에 닿으면 과매수, 하단 밴드에 닿으면 과매도로 해석합니다. 실무에서는 어떻게 활용할까요?
금융에서는 리스크 관리에 필수입니다. 변동성이 높아지면 포지션을 줄이고, 변동성이 낮아지면 포지션을 늘리는 전략을 사용합니다.
변동성이 갑자기 높아지면 시장에 무슨 일이 생긴 것이므로 주의해야 합니다. IT 운영에서는 이상 탐지에 활용합니다.
API 응답 시간의 이동표준편차가 평소의 두 배가 되면 알림을 보내는 식입니다. 절대값보다 변동성의 변화가 문제를 더 빨리 감지할 수 있습니다.
주의할 점이 있습니다. 윈도우 크기 선택이 중요합니다.
윈도우가 너무 작으면 노이즈에 민감하고, 너무 크면 변화를 감지하는 데 시간이 걸립니다. 도메인 지식과 실험을 통해 적절한 크기를 찾아야 합니다.
또한 윈도우 시작 부분에는 NaN이 발생합니다. 20일 이동평균은 처음 19일간은 계산할 수 없기 때문입니다.
min_periods 파라미터로 최소 필요 데이터 수를 조절할 수 있습니다. 김개발 씨는 이동표준편차 그래프를 그려 투자팀에 보여주었습니다.
"5월부터 변동성이 3배 이상 높아졌네요. 데이터가 말해주고 있어요."
실전 팁
💡 - 윈도우 크기는 분석 목적에 따라 조절하세요 (단기: 7일, 중기: 20일, 장기: 60일)
- 변동성이 급격히 증가하면 시장에 중요한 변화가 있다는 신호입니다
- rolling 외에도 ewm(지수가중이동평균)을 사용하면 최근 데이터에 더 가중치를 줄 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!