EduFlow

Section 4 · Analysis: Group, Join, Reshape

Time Series: resample and the dt Accessor

20 min3 min readPython exercise

Lesson at a glance

Turn timestamped orders into monthly, quarterly and weekly series with resample, compare periods with pct_change, smooth noise with rolling, and extract calendar parts with the dt accessor.

  1. 1resample: buckets of time
  2. 2Comparing periods
  3. 3The dt accessor: calendar parts

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

The monthly figures you have used since Lesson 1.3 came from somewhere: a few thousand timestamped orders, bucketed by calendar month. Time is the axis of almost every business question (“Is revenue growing?”, “Is this November normal?”, “When do people shop?”), and pandas has tools built for it. Before using them, you need one row per order with its revenue and timestamp.

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
order_revenue = items.groupby("order_id", as_index=False)["revenue"].sum()

orders = orders.merge(order_revenue, on="order_id", how="left", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]
print(orders[["order_id", "order_date", "revenue"]].head(3))

Every run block below starts with these lines, shortened to keep them readable. In your own notebook you would run them once.

resample: buckets of time

resample groups rows into regular time periods. It needs the dates in the index:

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
orders = orders.merge(items.groupby("order_id", as_index=False)["revenue"].sum(), on="order_id", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]

by_time = orders.set_index("order_date")
monthly = by_time.resample("ME")["revenue"].sum()
quarterly = by_time.resample("QE")["revenue"].sum()
print(len(monthly), "months")
print(monthly.tail(4).round(0))
print(quarterly.round(0))

The string is a frequency alias: "D" for days, "W" for weeks ending Sunday, "ME" for month ends, "QE" for quarter ends, "YE" for year ends. pandas 2.2 renamed "M", "Q" and "Y" to the E versions to make clear that the labels are period ends, and the old names now raise a deprecation warning. Periods are a different object and keep the short names: to_period("M") and to_period("Q") are correct, because a period stands for the whole month or quarter rather than its last day. The quarterly series is the one the head of growth is staring at: 80,523 dollars in Q3 2025, then 146,825 in Q4.

resample versus to_period

You can also group by a period: orders.groupby(orders["order_date"].dt.to_period("M"))["revenue"].sum(). The result looks the same here, with labels like 2025-11 instead of 2025-11-30. The difference shows when a period has no data: resample includes it with a sum of 0, while grouping skips it entirely. For charts and growth rates you want every period present, so resample is the safer default.

A timeline from October to December with order dots scattered along it. Vertical lines split it into three monthly buckets. Each bucket is labelled with its last day: 2025-10-31, 2025-11-30 and 2025-12-31, with the summed revenue under each label. October November December 2025-10-31 2025-11-30 2025-12-31 30,339 75,334 41,153
resample puts each timestamp into a fixed bucket and labels the bucket by its end

Comparing periods

Raw series answer “how much”; changes answer “how fast”:

Python
import pandas as pd

items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
orders = orders.merge(items.groupby("order_id", as_index=False)["revenue"].sum(), on="order_id", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]
by_time = orders.set_index("order_date")

quarterly = by_time.resample("QE")["revenue"].sum()
print(pd.DataFrame({
    "revenue": quarterly.round(0),
    "vs_prev_quarter": quarterly.pct_change().round(3),
    "vs_year_ago": quarterly.pct_change(4).round(3),
}))
monthly = by_time.resample("ME")["revenue"].sum()
print(monthly.rolling(3).mean().tail(3).round(0))

pct_change() divides each value by the previous one and subtracts 1. Q4 2025 is up 82% on Q3, which is the jump everyone noticed. But pct_change(4) compares each quarter with the same quarter a year earlier, and that column tells a calmer story: Q4 2025 is up 180% on Q4 2024, while Q1, Q2 and Q3 grew by 200%, 145% and 165%. Q4 grew about as fast as the rest of the year. A large part of the “jump” is a seasonal pattern that was there in 2024 too: Q4 2024 was 73% above Q3 2024.

rolling(3).mean() averages each month with the two before it, which smooths out single spikes such as November. Use it to see a trend; use the raw series to see events.

The dt accessor: calendar parts

For patterns within a period, such as days of the week or hours of the day, extract the part with .dt and group by it:

Python
import pandas as pd

orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
orders = orders[orders["status"] != "cancelled"]
d = orders["order_date"]
print(orders.groupby(d.dt.day_name()).size().sort_values())
print(orders.groupby(d.dt.hour).size().head(4))
print(d.dt.to_period("Q").value_counts().sort_index().tail(2))

Orders are spread fairly evenly across the week, from 241 on Mondays to 298 on Tuesdays and Saturdays, so there is no strong weekday effect to explain. Other useful parts: dt.year, dt.quarter, dt.month, dt.isocalendar().week, dt.date and dt.normalize() (midnight of the same day, which you used in Lesson 3.4).

Daily resampling is where assumptions get tested. by_time.resample("D").size() gives one count per calendar day, 729 of them from the first order to the last, and two facts fall out at once: 119 days had no orders at all (in a series built with groupby, those days would simply be missing), and the busiest single day was 10 November 2025 with 16 orders, almost three weeks before Black Friday (28 November). The other top days, 6, 8 and 26 November, are spread across the month too. Whatever drove November, it was not one big promotional day; keep that in mind for the case study.

In the exercise you build the monthly series and the year-over-year view yourself. Next you rank and bin customers, turning a continuous measure like total spend into segments.

Key takeaways

  • df.set_index("order_date").resample("ME")["revenue"].sum() buckets rows into calendar months; "QE", "W" and "D" give quarters, weeks and days.
  • In pandas 2.2 and later, resample writes month and quarter ends as "ME" and "QE"; the old "M" and "Q" aliases are deprecated there, while periods such as to_period("M") keep the short names.
  • resample includes empty periods (a month with no orders shows 0), while grouping by dt.to_period("M") only lists periods that have data.
  • pct_change() compares each period with the previous one, and pct_change(4) on quarters compares with the same quarter a year earlier.
  • Resample labels mark the end of each bin, so a weekly series can show a 2026 label for orders placed in December 2025; check the first and last periods for partial data.

Further reading

Check your understanding

3 questions

Question 1 of 3

Quarterly revenue is in q, oldest first. Which expression gives each quarter’s growth versus the same quarter one year earlier?

Question 2 of 3

A weekly order count made with resample("W") ends with a week labelled 2026-01-04 that has only 11 orders. What is going on?

Question 3 of 3

Which code gives the number of orders per weekday name?

Practice

Python exercise

Monthly, quarterly, year over year

orders has one row per non-cancelled order, with order_date and revenue. Build monthly (revenue per calendar month, month-end labels) and monthly_orders (number of orders per month) with resample. Build quarterly (revenue per quarter) and yoy, its growth versus the same quarter a year earlier as a fraction (1.0 means +100%). Finally set best_weekday to the name of the weekday with the highest total revenue.

Data files: order_items.csv, orders.csv

import pandas as pd

items = pd.read_csv("order_items.csv")
orders = pd.read_csv("orders.csv", parse_dates=["order_date"])
items["revenue"] = items["quantity"] * items["unit_price"] * (1 - items["discount"])
order_revenue = items.groupby("order_id", as_index=False)["revenue"].sum()
orders = orders.merge(order_revenue, on="order_id", how="left", validate="one_to_one")
orders = orders[orders["status"] != "cancelled"]

# 1. Monthly revenue (Series, one value per calendar month, month-end labels)
monthly = None

# 2. Monthly number of orders
monthly_orders = None

# 3. Quarterly revenue, and its growth versus the same quarter a year earlier
quarterly = None
yoy = None

# 4. Weekday name with the highest total revenue, e.g. "Monday"
best_weekday = None
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

(no output)

Checks

  • monthly has 24 months, with November 2025 at about 75,334
  • monthly_orders shows 325 orders in November 2025
  • quarterly has 8 quarters, ending at about 146,825
  • yoy is 1.795 for Q4 2025 and empty for 2024
  • best_weekday is the top weekday by revenue