Skip to content

pandas resample

Re-bucket time-series data into regular periods — daily, monthly, quarterly, yearly.

resample is groupby for time. Where groupby splits rows by the value in a column, resample splits them by when they happened, filling in the calendar for you. Events that arrive irregularly — earthquakes, sales, sensor readings — become a tidy row per month whether or not anything happened in a given month.

Official documentation: DataFrame.resample

The forms worth knowing

resample needs a DatetimeIndex, so it almost always follows set_index:

df.set_index('time').resample('ME').sum()      # month end
df.set_index('time').resample('D').mean()      # daily
df.set_index('time').resample('QE').size()     # quarter end, count of rows
df.set_index('time').resample('YE').max()      # year end

# Combine with agg for several answers at once
df.set_index('time').resample('ME').agg(
    quakes=('mag', 'size'),
    strongest=('mag', 'max'),
)

The frequency codes

Nobody remembers these, and Pandas 3 changed several of them. The ones worth knowing:

Code Means Example
s second .resample('30s') — half-minute buckets
min minute .resample('15min') — quarter-hourly
h hour .resample('6h') — four buckets a day
D calendar day .resample('D')
B business day .resample('B') — skips weekends
W week, ending Sunday .resample('2W') — fortnightly
ME month end .resample('ME') — labelled 2024-01-31
MS month start .resample('MS') — labelled 2024-01-01
QE quarter end .resample('QE')
QS quarter start .resample('QS')
YE year end .resample('YE')
YS year start .resample('YS')

Any code takes a multiplier, which is where 2W and 6h above come from.

The end/start pairs are the ones that catch people. ME and MS produce the same buckets — they differ only in whether each one is labelled with the first or last day of the month. That difference then shows up on every chart axis you draw afterwards.

Seven codes were retired in Pandas 3 and now raise ValueError: Invalid frequency rather than warning: T, H, S, M, Q, Y and A. They became min, h, s, ME, QE, YE and YE. Almost every tutorial written before 2025 uses the old spellings, so this is the first thing to fix when porting code that used to work.

The full list — including anchored offsets like W-FRI and business-quarter variants — is in the Pandas user guide: Offset aliases. Worth bookmarking; it is the page everyone re-finds every few months.

A worked example, on real data

The USGS publishes every earthquake it records as CSV, through a public API with no key — the source Bamboo Weekly #3 worked with.

Earthquakes arrive whenever they arrive. To ask "how did 2024 unfold, month by month?" you need them bucketed:

import pandas as pd

url = ('https://earthquake.usgs.gov/fdsnws/event/1/query.csv'
       '?starttime=2024-01-01&endtime=2024-12-31&minmagnitude=6')

(
    pd.read_csv(url, usecols=['time', 'mag'], parse_dates=['time'])
    .set_index('time')
    .resample('ME')
    .agg(quakes=('mag', 'size'),
         strongest=('mag', 'max'))
    .head(6)
)

Which gives:

                           quakes  strongest
time
2024-01-31 00:00:00+00:00      12        7.5
2024-02-29 00:00:00+00:00       4        6.3
2024-03-31 00:00:00+00:00       8        6.9
2024-04-30 00:00:00+00:00      12        7.4
2024-05-31 00:00:00+00:00       8        6.6
2024-06-30 00:00:00+00:00       7        7.2

Three steps: make the timestamp the index, bucket by month, then aggregate. The index labels are month ends — that is what the E in ME means, and it is why February reads 2024-02-29 rather than 2024-02-28. It was a leap year, and resample knew.

Three mistakes people make

Using the retired 'M' and 'Y' codes. In Pandas 3 these raise ValueError: Invalid frequency: M rather than warning. If you are porting code that worked last year, this is the first thing to fix.

Calling resample without a DatetimeIndex. It has to know which column is time. Either set_index first, or pass on=df.resample('ME', on='time') — but the index form chains better and makes the intent obvious.

Forgetting that empty periods still appear. A month with no events produces a row with 0 or NaN, which is usually the point — a gap in the calendar is a finding. If you only want periods that contain data, groupby on .dt.to_period('M') gives you that instead.

Watch it

Resampling? How offsets are changing in Pandas 3 covers the frequency-code change directly, and Pandas time series superpowers: Why datetime indexes matter explains why the index has to be a datetime in the first place.

Practice it

Work through a resample exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/resample/

Go deeper

Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.

See it on real data

Below are the 47 Bamboo Weekly exercises that use resample on real-world data — try each one, then study the worked solution.

Part of the Pandas Methods Index. See also practice by skill.