Time series resampling

pandas · Advanced · 15 min

Turning event streams into daily, weekly or monthly series is the standard first step of time-series analysis.

`resample(rule)` is a groupby over time buckets. It requires a DatetimeIndex (or on="ts" for a column) and a frequency alias.

Downsampling (to a coarser frequency) aggregates:

daily = events.resample("D")["amount"].sum()
monthly = events.resample("ME").agg({"amount": "sum", "user": "nunique"})

.ohlc() gives open/high/low/close per bucket, useful for prices. Empty buckets appear as well (sum gives 0, mean gives NaN), which a groupby on the date wouldn't do. That gap-filling is often exactly what you want for charts and models.

Upsampling (to a finer frequency) creates new empty slots that you fill: resample("h").ffill(), .bfill(), .interpolate(), or .asfreq() to just insert NaN.

Bucket edges matter. Defaults depend on the frequency: for "D" and "h" buckets are closed and labelled on the left; for end-anchored rules like "ME", "QE", "W" they're closed and labelled on the right. closed= and label= override this, and origin=/offset= shift where buckets start (e.g. days starting at 06:00). In pandas 2.2+ month-end is "ME" and month-start "MS"; old "M" is deprecated.

Two alternatives with the same machinery:

  • df.groupby(["store", pd.Grouper(key="ts", freq="W")])["sales"].sum() resamples per store.
  • df.groupby(df.ts.dt.to_period("M")) groups by calendar periods, giving period labels like 2024-03.

Time zones and daylight saving apply: resampling a tz-aware series to days uses local days, so some days have 23 or 25 hours. Convert to UTC first if you need uniform buckets.

Example

import pandas as pd

events = pd.DataFrame({
    "ts": pd.to_datetime(["2024-01-01 09:00", "2024-01-01 17:30", "2024-01-03 08:00",
                          "2024-02-10 12:00", "2024-02-11 13:00"]),
    "store": ["a", "b", "a", "a", "b"],
    "amount": [20, 35, 10, 50, 5],
}).set_index("ts")

daily = events["amount"].resample("D").sum()
print(daily.loc["2024-01-01":"2024-01-03"].tolist())   # [55, 0, 10]  empty day kept

monthly = events.resample("ME").agg({"amount": "sum", "store": "nunique"})
print(monthly)
#             amount  store
# ts
# 2024-01-31      65      2
# 2024-02-29      55      2

hourly = pd.Series([1.0, 4.0], index=pd.to_datetime(["2024-01-01 00:00", "2024-01-01 03:00"]))
print(hourly.resample("h").interpolate().tolist())     # [1.0, 2.0, 3.0, 4.0]

per_store = events.groupby(["store", pd.Grouper(freq="MS")])["amount"].sum()
print(per_store.tolist())                 # [30, 50, 35, 5]  (a Jan, a Feb, b Jan, b Feb)

Remember

  • resample(rule) groups by time buckets on a DatetimeIndex
  • Downsample with aggregations; upsample with ffill, interpolate or asfreq
  • Empty buckets are included, unlike a plain groupby
  • Use pd.Grouper(freq=...) inside groupby to resample per entity

Watch out for

  • Using the deprecated 'M' alias or assuming left-labelled buckets for month-end rules, then misreading which period a value belongs to.

Check yourself

  1. How does `resample('D').sum()` differ from grouping by `dt.date` and summing?
  2. You have hourly readings and need 15-minute values by linear interpolation. Which call?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pandas.pydata.org/docs/user_guide/timeseries.html#resampling

Builds on

Unlocks

Nothing builds on this yet.