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.
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.
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:
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.
Comparing periods
Raw series answer “how much”; changes answer “how fast”:
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:
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,
resamplewrites month and quarter ends as"ME"and"QE"; the old"M"and"Q"aliases are deprecated there, while periods such asto_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, andpct_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.