Time Series
Pandas treats time-series as a first-class citizen: DatetimeIndex, frequency strings, resampling, rolling windows, offset arithmetic. Get the index right and operations like “weekly average” or “30-day rolling sum” become one-liners.
DatetimeIndex, resample, rolling, tz
EXAMPLE
import pandas as pd
import numpy as np
# 1) Build a time-series DataFrame
idx = pd.date_range(start='2024-01-01', periods=24, freq='h')
df = pd.DataFrame({
'orders': np.random.randint(0, 100, 24),
'revenue': np.random.uniform(0, 10_000, 24),
}, index=idx)
df.index.name = 'ts'
print(df.head())
# 2) Parsing dates
df = pd.read_csv('events.csv', parse_dates=['occurred_at'], index_col='occurred_at')
# OR
df['occurred_at'] = pd.to_datetime(df['occurred_at'], utc=True)
df = df.set_index('occurred_at').sort_index()
# 3) Frequency strings (offsets)
# 'D' day
# 'B' business day
# 'W' week (default Sunday)
# 'W-MON' week ending Monday
# 'M' month-end
# 'MS' month-start
# 'Q' quarter-end
# 'A' year-end
# 'h' hour
# 'min' or 'T' minute
# 's' second
# '15min' every 15 min
# 'D'+'h' combine via offsets
pd.date_range('2024-01-01', '2024-01-10', freq='D')
pd.date_range('2024-01-01', periods=10, freq='15min')
# 4) Slicing by date — partial-string indexing
df['2024-01-15'] # whole day
df['2024-01'] # whole month
df['2024-01-15':'2024-01-20'] # range (inclusive both ends)
df.loc['2024-01-15 09:00':'2024-01-15 17:00']
# 5) Resample — change frequency
hourly = df['orders']
daily = hourly.resample('D').sum()
weekly = hourly.resample('W').mean()
monthly = hourly.resample('MS').sum()
# Multi-aggregation
df.resample('D').agg({
'orders': ['sum', 'mean'],
'revenue': ['sum', 'max'],
})
# OHLC bar (open-high-low-close) — finance idiom
df['revenue'].resample('D').ohlc()
# 6) Rolling windows
df['orders'].rolling(window=24).mean() # 24-hour moving average
df['orders'].rolling(window='7D').sum() # time-based window
df['orders'].rolling(24, min_periods=12).mean() # tolerate partial windows
df['orders'].rolling(24).agg(['mean', 'min', 'max', 'std'])
# Centered window
df['orders'].rolling(24, center=True).mean()
# 7) Expanding windows — from start to current
df['orders'].expanding().sum() # cumulative
df['orders'].expanding(min_periods=10).mean() # cumulative average
# 8) Shifting + diffs
df['orders_lag_1'] = df['orders'].shift(1) # previous value
df['orders_diff'] = df['orders'].diff() # x - x_prev
df['orders_pct'] = df['orders'].pct_change() # (x - x_prev) / x_prev
df['orders_yoy'] = df['orders'].pct_change(periods=365 * 24) # year-over-year (hourly data)
# 9) Resample + fill missing periods
df2 = df.resample('D').sum() # daily sum (zero days = 0)
df3 = df.asfreq('h', fill_value=0) # force frequency, fill gaps
df4 = df.resample('h').asfreq() # resample then leave NaN
# Forward / back fill
df['orders'].resample('h').ffill()
df['orders'].resample('h').bfill()
# 10) Time zones
idx = pd.date_range('2024-01-01', periods=24, freq='h', tz='UTC')
df.tz_localize('UTC') # naive -> UTC
df.tz_convert('Australia/Sydney') # convert to Sydney time
df.tz_localize(None) # strip tz info
# Daylight saving: tz-aware operations handle DST transitions correctly.
# 11) Offsets — add business days, month-ends
from pandas.tseries.offsets import BDay, MonthEnd, Week
next_bday = pd.Timestamp('2024-01-05') + BDay(1)
end_of_month = pd.Timestamp('2024-01-15') + MonthEnd(0)
next_friday = pd.Timestamp('2024-01-01') + Week(weekday=4)
# Custom holiday calendar
from pandas.tseries.holiday import USFederalHolidayCalendar
cal = USFederalHolidayCalendar()
biz_days = pd.bdate_range('2024-01-01', '2024-01-31', freq=pd.offsets.CustomBusinessDay(calendar=cal))
# 12) Group by time period
df.groupby(df.index.date)['orders'].sum() # by date
df.groupby(df.index.hour)['orders'].mean() # by hour-of-day
df.groupby([df.index.year, df.index.month])['revenue'].sum()
# 13) Truncation + flooring
ts = pd.Timestamp('2024-01-15 13:42:17')
ts.floor('h') # 2024-01-15 13:00:00
ts.ceil('h') # 2024-01-15 14:00:00
ts.round('h') # 2024-01-15 14:00:00
df.index.floor('D')
# 14) Date range patterns
pd.date_range('2024-01-01', '2024-01-31') # daily
pd.date_range('2024-01-01', periods=12, freq='MS') # 12 month-starts
pd.bdate_range('2024-01-01', '2024-01-31') # business days
# 15) Time-series plot
import matplotlib.pyplot as plt
df['orders'].plot(figsize=(12, 4))
df['orders'].rolling('7D').mean().plot() # overlay 7-day MA
plt.show()
# 16) Statsmodels / Prophet — proper forecasting
# from statsmodels.tsa.arima.model import ARIMA
# model = ARIMA(df['orders'], order=(1, 1, 1)).fit()
# forecast = model.forecast(steps=24)
#
# from prophet import Prophet
# m = Prophet()
# m.fit(df.reset_index().rename(columns={'index': 'ds', 'orders': 'y'}))
# 17) Common bugs
# • Forgetting to sort index → resample / rolling produce garbage; df.sort_index()
# • Naive vs tz-aware datetimes mixed → TypeError; pick one and convert
# • Resample W defaults to Sunday-ending — use W-MON or W-FRI for business cycles
# • Daylight saving boundary → hour repeats or skips; use UTC for storage, local for display
# • parse_dates with format='%Y/%m/%d' assumed but column has dashes → silent NaT; specify format
# • Index not a DatetimeIndex → resample raises TypeError
# • Rolling on irregular index without time-based window → wrong window size
# • Year arithmetic across leap years — use pd.DateOffset(years=1), not Timedelta(days=365)
# • Storing dates as strings — lose comparison + sort semantics; always to_datetime
# • Doing time math with .dt.year * 12 + .dt.month → fragile; use Period or DateOffset
Why it matters
Make the index a DatetimeIndex, store in UTC, convert to local for display. resample changes frequency, rolling(\"7D\") opens a time-based window, shift/diff/pct_change handle lags, and partial-string indexing makes df["2024-01"] a one-liner for “whole month”. Sort the index before any of this and watch DST boundaries.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import pandas as pd
df = pd.read_csv('sales.csv', parse_dates=['date']).set_index('date')
monthly = df['amount'].resample('M').sum()
last_30 = df.last('30D')
Try it Yourself »
Discussion
Loading…