서비스 데이터에는 거의 항상 시간이 붙는다. 날짜별 가입자 수, 시간대별 API 요청 수, 일별 주문 금액, 서버 자원 사용률처럼 시간의 순서에 따라 값이 변하는 데이터를 다루려면 일반적인 표 조작만으로는 부족하다.
Pandas는 날짜 문자열을 실제 날짜 타입으로 바꾸고, 일정한 시간 간격을 만들고, 이전 시점과 비교하고, 시간 단위를 다시 구성하는 기능을 제공한다.
날짜 문자열
↓
DatetimeIndex
↓
시간 순서가 있는 Series / DataFrame
↓
shift / diff
↓
resample
↓
plot
시계열 데이터(Time Series Data) 는 시간 순서에 따라 기록된 관측값의 집합이다. 일반적인 테이블과 다른 점은 행의 순서 자체에 의미가 있다는 것이다.
예를 들어 API 요청 수가 다음과 같다고 하자.
| date | requests |
|---|---|
| 2026-09-01 | 1200 |
| 2026-09-02 | 1350 |
| 2026-09-03 | 1280 |
이 데이터에서 날짜 순서를 무시하면 증가·감소 추세를 제대로 해석하기 어렵다. 시계열을 다룰 때는 보통 시간 순서가 올바른지, 관측 간격이 일정한지를 먼저 확인해야 한다.
CSV나 API에서 날짜를 읽으면 문자열인 경우가 많다.
dates = [
"2026/01/01",
"2026/02/01",
"2026/03/01",
"2026/04/01"
]
문자열 상태에서는 날짜 간 차이나 월별 집계 같은 시간 연산을 하기 어렵다. 이럴 때 pd.to_datetime() 을 사용한다.
import pandas as pd
date_index = pd.to_datetime(dates)
print(date_index)
실습에서는 변환 결과가 다음처럼 DatetimeIndex 로 확인됐다.
DatetimeIndex([
'2020-01-01',
'2020-02-01',
'2020-03-01',
'2020-04-01'
], dtype='datetime64[ns]', freq=None)
freq=None은 값이 datetime으로 변환되었지만 일정한 주기 정보가 별도로 지정되지는 않았다는 뜻이다.
일반적인 날짜 표현은 다음처럼 구분자가 조금 달라도 변환할 수 있다.
pd.to_datetime([
"2026.01.01",
"2026.02.01",
"2026.03.01"
])
형식이 명확하다면 format을 지정할 수도 있다.
pd.to_datetime(
["2026-01-01", "2026-02-01"],
format="%Y-%m-%d"
)
날짜를 인덱스로 사용하면 Pandas의 시계열 연산을 활용하기 편하다.
import numpy as np
import pandas as pd
values = np.array([8, 6, 7, 3])
date_index = pd.to_datetime([
"2026-01-01",
"2026-02-01",
"2026-03-01",
"2026-04-01"
])
series = pd.Series(
values,
index=date_index
)
print(series)
구조는 다음처럼 된다.
2026-01-01 8
2026-02-01 6
2026-03-01 7
2026-04-01 3
이제 shift(), diff(), resample() 같은 시계열 기능을 자연스럽게 사용할 수 있다.
특정 기간의 날짜 인덱스를 만들고 싶다면 pd.date_range() 를 사용한다.
pd.date_range(
start="2026-01-01",
end="2026-01-10"
)
종료일 대신 생성할 개수를 지정할 수도 있다.
pd.date_range(
start="2026-01-01",
periods=7
)
freq는 생성되는 날짜의 간격을 정한다.
pd.date_range(
start="2026-01-01",
periods=6,
freq="MS"
)
대표적으로 다음과 같은 주기를 자주 접한다.
| freq | 의미 |
|---|---|
D | 일 단위 |
W | 주 단위 |
W-MON | 월요일 기준 주 단위 |
MS | 월 시작일 |
M | 월 말일 계열 |
B | 주말을 제외한 영업일 계열 |
Pandas 버전에 따라 일부 주기 별칭의 권장 표기가 달라질 수 있으므로 경고가 발생한다면 사용하는 버전의 offset alias 문서를 확인하면 된다.
시계열에서는 현재 값과 이전 값을 비교할 일이 많다.
오늘 매출 vs 어제 매출
이번 달 가입자 vs 지난달 가입자
현재 트래픽 vs 1시간 전 트래픽
이럴 때 shift() 를 사용할 수 있다.
np.random.seed(1)
values = np.random.randn(12)
index = pd.date_range(
"2026-01-01",
periods=12,
freq="MS"
)
ts = pd.Series(
values,
index=index
)
한 기간 이전 값을 현재 행에 맞추려면 다음처럼 작성한다.
ts.shift(1)
개념적으로는 다음과 같다.
원본 shift(1)
1월 A NaN
2월 B A
3월 C B
4월 D C
실행 결과에서도 shift(1)은 첫 행이 NaN, shift(4)는 첫 4개 행이 NaN이 되는 것을 확인할 수 있었다.
ts.shift(4)
ts.shift(1)
은 값을 한 행 이동시키고 기존 인덱스는 유지한다.
반면:
ts.shift(
1,
freq="W"
)
처럼 freq를 지정하면 시간 인덱스 자체를 주어진 규칙에 따라 이동시킨다.
| 사용 방식 | 의미 |
|---|---|
shift(1) | 1기간 전 값을 현재 시점과 맞춤 |
shift(3) | 3기간 전 값을 현재 시점과 맞춤 |
shift(1, freq=...) | 날짜 인덱스를 시간 규칙만큼 이동 |
전일 대비나 전월 대비 계산에서는 보통 shift(periods) 방식이 많이 쓰인다.
현재 값과 이전 값의 차이 자체가 필요하다면 diff() 가 더 간단하다.
ts.diff(1)
계산식은 다음과 같다.
현재 값 - 1기간 전 값
즉 개념적으로는:
ts.diff(1)
과
ts - ts.shift(1)
이 같은 형태의 변화량을 만든다고 이해할 수 있다.
실습에서는 첫 두 값이 약 1.624345, -0.611756일 때:
ts[1] - ts[0]
결과가 약:
-2.2361017773
으로 나왔고, ts.diff(1) 결과와 같은 값을 확인할 수 있었다.
3기간 전과 비교하려면 다음처럼 작성한다.
ts.diff(3)
처음 3개의 값은 비교 대상이 없으므로 NaN이 생긴다.
diff()는 일별 매출 증감, 트래픽 변화량, 사용자 수 변화처럼 기간별 변화 자체가 중요한 데이터에 유용하다.
시계열 데이터를 분석할 때는 원본 시간 단위를 그대로 쓰지 않고 목적에 맞게 바꿀 때가 많다.
예를 들어 일별 데이터를 월별로 요약할 수 있다.
일별 데이터
↓
월별 구간으로 재구성
↓
평균 / 합계 / 최대값 계산
↓
월별 데이터
Pandas에서는 resample() 을 사용한다.
365일치 데이터를 만든다고 하자.
np.random.seed(1)
values = np.random.randn(365)
index = pd.date_range(
"2026-01-01",
periods=365,
freq="D"
)
daily = pd.Series(
values,
index=index
)
일별 데이터를 월별 평균으로 바꾸면:
monthly = (
daily
.resample("M")
.mean()
)
여러 관측값이 하나의 더 큰 시간 구간으로 합쳐지므로 Down-sampling이다.
집계 함수는 데이터 의미에 따라 달라진다.
daily.resample("M").mean()
daily.resample("M").sum()
daily.resample("M").max()
예를 들면:
mean()sum()max()처럼 선택할 수 있다.
반대로 주별 데이터를 일별 데이터로 바꾸면 원래 없던 날짜가 새로 생성된다.
weekly = pd.Series(
[10, 20, 15],
index=pd.date_range(
"2026-01-04",
periods=3,
freq="W"
)
)
daily = weekly.resample("D")
이 경우 새로 생긴 날짜에는 원래 값이 없기 때문에 결측값이 생긴다.
ffill() 은 앞의 값을 다음 관측 시점 전까지 전달한다.
weekly.resample("D").ffill()
1/04 10
1/05 10
1/06 10
...
1/11 20
실행 결과에서도 주간 값이 다음 주의 실제 관측값이 등장하기 전까지 유지되는 것을 확인할 수 있었다.
bfill() 은 뒤에 있는 값을 앞쪽 결측 구간에 채운다.
weekly.resample("D").bfill()
1/04 10
1/05 20
1/06 20
...
1/11 20
ffill()과 bfill()은 편리하지만, 새로 채워진 값은 실제 측정값이 아니라 규칙으로 보완한 값이라는 점을 기억해야 한다.
| 구분 | Down-sampling | Up-sampling |
|---|---|---|
| 시간 간격 | 더 크게 만듦 | 더 작게 만듦 |
| 예시 | 일별 → 월별 | 주별 → 일별 |
| 데이터 개수 | 보통 감소 | 보통 증가 |
| 주요 처리 | 집계 | 결측값 처리 |
| 자주 쓰는 방식 | mean, sum, max | ffill, bfill |
핵심은 데이터 수를 단순히 늘리고 줄이는 것이 아니라 시간 해상도를 분석 목적에 맞게 조정하는 것이다.
시계열 데이터는 표보다 선 그래프로 봤을 때 흐름을 파악하기 쉽다.
Pandas의 DataFrame은 plot() 메서드를 제공한다.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
np.random.seed(1)
values = np.random.randn(
365,
3
)
index = pd.date_range(
start="2026-01-01",
periods=365,
freq="D"
)
ts_df = pd.DataFrame(
values,
index=index,
columns=["API-A", "API-B", "API-C"]
)
ts_df.plot()
plt.show()
DatetimeIndex를 사용하면 x축이 시간으로 처리된다.
시계열 그래프에서는 주로 다음을 확인한다.
시각화를 위해 다음 코드를 실행했을 때:
import matplotlib.pyplot as plt
실습 환경에서는 다음 오류가 발생했다.
ModuleNotFoundError:
No module named 'matplotlib'
이 오류는 Python 문법 문제가 아니라 현재 실행 중인 Python 환경에 matplotlib가 설치되어 있지 않다는 의미다.
Jupyter Notebook에서는 다음처럼 설치할 수 있다.
%pip install matplotlib
그 뒤 다시 import를 실행했을 때 정상 수행되었고, DataFrame의 plot()도 실행할 수 있었다.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
일반 터미널에서는 다음처럼 설치할 수 있다.
python -m pip install matplotlib
Conda 환경을 사용한다면 먼저 해당 환경을 활성화한다.
conda activate <환경이름>
python -m pip install matplotlib
또는:
conda install matplotlib
설치했는데도 같은 오류가 반복된다면 터미널의 Python 환경과 Jupyter 커널 환경이 같은지 확인해야 한다.
시간대별 API 요청 수가 있다고 가정해보자.
traffic = pd.DataFrame({
"timestamp": [
"2026-09-01 09:00",
"2026-09-01 10:00",
"2026-09-01 11:00",
"2026-09-02 09:00"
],
"requests": [
1200,
1480,
1520,
1310
]
})
먼저 문자열 시간을 변환한다.
traffic["timestamp"] = pd.to_datetime(
traffic["timestamp"]
)
그리고 시간을 인덱스로 설정한다.
traffic = traffic.set_index(
"timestamp"
)
이제 하루 단위 총 요청 수를 만들 수 있다.
daily_requests = (
traffic["requests"]
.resample("D")
.sum()
)
전일 대비 변화량도 쉽게 계산할 수 있다.
daily_change = daily_requests.diff(1)
시각화도 바로 이어진다.
daily_requests.plot()
전체 흐름은 다음처럼 정리할 수 있다.
로그의 문자열 timestamp
↓ pd.to_datetime()
DatetimeIndex
↓ resample()
일별 / 주별 / 월별 지표
↓ diff() / shift()
이전 기간과 비교
↓ plot()
시간 변화 확인
화면에 날짜처럼 보여도 문자열이면 시간 연산이 제한된다. 먼저 dtype을 확인하는 습관이 좋다.
shift()는 비교할 값을 옮겨 맞추는 기능이고, diff()는 두 시점의 차이를 바로 계산한다.
resample()은 시간 기준으로 구간을 다시 만들기 때문에 DatetimeIndex 같은 시간 기반 인덱스를 사용하는 구조가 핵심이다.
ffill()이나 bfill()로 생성된 값은 실제 관측된 값과 의미가 다를 수 있다.
매출은 합계가 자연스러울 수 있지만 응답시간은 평균이나 중앙값이 더 적절할 수 있다.
pd.to_datetime()으로 문자열 날짜를 datetime 타입으로 변환할 수 있다.pd.date_range()로 일정한 시간 간격의 날짜 인덱스를 생성할 수 있다.freq로 일, 주, 월 등 관측 간격을 정할 수 있다.shift()는 이전 시점의 값을 현재 시점과 맞출 때 유용하다.diff(n)은 현재 값 - n기간 전 값을 계산한다.resample()로 시간 단위를 다시 구성할 수 있다.ffill()과 bfill()은 업샘플링 후 빈 구간을 채우는 대표적인 방법이다.plot()으로 시간 흐름을 쉽게 시각화할 수 있다.ModuleNotFoundError: No module named 'matplotlib'가 발생하면 현재 실행 환경의 패키지 설치 여부를 확인한다.Pandas의 시계열 기능은 날짜를 저장하는 것에서 끝나지 않는다. 시간 단위를 맞추고, 이전 시점과 비교하고, 변화량을 계산하고, 시간 해상도를 바꾸고, 시각적으로 흐름을 확인하는 과정까지 자연스럽게 연결된다.
특히 to_datetime → date_range → shift/diff → resample → plot 흐름을 이해하면 로그 분석, 트래픽 모니터링, 매출 추이, 사용자 행동 데이터 같은 실제 서비스 데이터에도 그대로 응용할 수 있다.
시계열 데이터를 다룰 때는 함수 사용법뿐 아니라 시간 순서, 관측 간격, 새로 생성된 값의 의미까지 함께 생각하는 것이 중요하다.