Changing the frequency
Data rarely comes at the frequency you need. Ticks arrive by the millisecond, a model wants daily bars, a report wants months. Resampling regroups a time series into new periods. Going from fine to coarse (days to months) is downsampling: each new period holds several old observations, so you must say how to combine them. Going from coarse to fine (weeks to days) is upsampling: there are new periods with no data, so you must say what to put in them.
import pandas as pd
daily = pd.Series(
[100.0, 101, 103, 102, 104, 105, 103, 106],
index=pd.bdate_range("2026-02-25", periods=8), # Wed 25 Feb ... Fri 6 Mar
name="close",
)
print(daily.resample("ME").last()) # the last price of each month ("ME" = month end)
# 2026-02-28 103.0
# 2026-03-31 106.0
# Freq: ME, Name: close, dtype: float64
print(daily.resample("W-FRI").last()) # the last price of each week, weeks ending on Friday
# 2026-02-27 103.0
# 2026-03-06 106.0
# Freq: W-FRI, Name: close, dtype: float64Choosing the aggregation
series.resample("ME") alone does nothing: it only describes the periods. The aggregation that follows decides the value, and it must match the meaning of the data. A price is a snapshot, so a month is represented by its last (or first) value. A volume is an amount traded, so a month is the sum. A range uses max and min. Labels follow the rule: "ME" labels a month with its last calendar day, even if that day is a Saturday, and "W-FRI" labels a week with its Friday.
volume = pd.Series([10, 20, 30, 40, 50, 60, 70, 80], index=daily.index)
print(volume.resample("ME").sum().to_dict())
# {Timestamp('2026-02-28 00:00:00'): 60, Timestamp('2026-03-31 00:00:00'): 300}
# Several summaries at once
print(daily.resample("ME").agg(["first", "last", "max"]))
# first last max
# 2026-02-28 100.0 103.0 103.0
# 2026-03-31 102.0 106.0 106.0Bars from ticks, and upsampling
Charts and models use OHLC bars: the open (first price), high, low and close (last price) of each period. resample("D").ohlc() builds them from intraday prices in one call. Calendar days without any trade, such as weekends, still appear as rows of NaN, so finish with dropna() when you only want days that traded. Upsampling goes the other way: resample("D").ffill() repeats the last known value on the days without data, which is what a price quoted weekly really means. Never use bfill for prices: it copies a future value backwards and leaks tomorrow's information into today.
times = ["2026-03-05 09:30", "2026-03-05 11:00", "2026-03-05 15:59",
"2026-03-09 09:30", "2026-03-09 12:00", "2026-03-09 15:59"]
ticks = pd.Series([100.0, 101.5, 99.8, 100.7, 102.0, 101.0], index=pd.to_datetime(times))
bars = ticks.resample("D").ohlc()
print(bars)
# open high low close
# 2026-03-05 100.0 101.5 99.8 99.8
# 2026-03-06 NaN NaN NaN NaN
# 2026-03-07 NaN NaN NaN NaN
# 2026-03-08 NaN NaN NaN NaN
# 2026-03-09 100.7 102.0 100.7 101.0
print(bars.dropna().index.strftime("%d %b").tolist()) # ['05 Mar', '09 Mar'] only the days that traded# Upsampling: observations on the 2nd, 6th and 10th, spread over every day with ffill
weekly = pd.Series([100.0, 103.0, 101.0], index=pd.to_datetime(["2026-03-02", "2026-03-06", "2026-03-10"]))
print(weekly.resample("D").ffill().tolist())
# [100.0, 100.0, 100.0, 100.0, 103.0, 103.0, 103.0, 103.0, 101.0]
print(int(weekly.resample("D").mean().isna().sum())) # 6 without a fill rule the new days are empty"ME" does, use ffill (never bfill) for upsampling, and shift a monthly signal forward before using it on daily data.series.resample(rule)describes the periods ("D","W-FRI","ME","h"); an aggregation after it computes the values.- Prices:
lastorohlc(); volumes:sum; empty periods show up asNaN, sodropna()removes them. - Upsampling needs a fill rule:
ffill()repeats the last known value; neverbfill()for prices, it leaks the future.
Next up: rolling windows, statistics such as moving averages computed over a sliding window of recent periods.
Lab · what's graded
- Take the last price of each month with
month_end_close25 pts - Total the volume of each week with
weekly_volume25 pts - Build daily bars from intraday prices with
daily_ohlc25 pts - Spread irregular observations over every day with
fill_to_daily25 pts