Data Analysis with Python (pandas, polars, DuckDB) Module 8 – Dates and time series
Resampling, frequencies and Indian financial years
Resample daily data to weeks, months and quarters with the pandas 3 frequency aliases, report by Indian financial year and flag the gaps bins expose.
What you will learn
- Aggregate time series to days, weeks, months and quarters with resample
- Use the pandas 3 frequency aliases (ME, QE, YE, h) correctly
- Report by Indian financial year (April to March) and its quarters
- Detect, fill or flag the gaps that resampling exposes
Before you start
What you’ll learn · Before you start
What you will learn
- Aggregate time series to days, weeks, months and quarters with resample
- Use the pandas 3 frequency aliases (ME, QE, YE, h) correctly
- Report by Indian financial year (April to March) and its quarters
- Detect, fill or flag the gaps that resampling exposes
Before you start
On this page
The canteen’s 313 recorded days become four quarterly totals.
resampling: Grouping time-indexed observations into new time bins, or introducing new timestamps when changing frequency; the labels and aggregation define the result. changes the frequency of a time series. Downsampling turns many short intervals into fewer long ones, such
as days into weeks, months or quarters, and aggregates each bin with a sum, a mean or a count; upsampling goes the
other way and creates empty slots that something must fill. In pandas, sales.resample("ME").sum() works like a
group-by on time. Its rules are frequencies written as short strings, and pandas 3 removed the old spellings "M",
"Q", "Y" and "H", which now raise a ValueError. Indian financial years run from April to March.
"QE-MAR" groups quarter-end bins; "Q-MAR" Periods supply April-based quarter numbers and the financial year’s
ending year. A bin covers a stretch of time whether or not any rows fell into it, which is why resampling also
exposes missing days.
Choose bins before adding canteen sales
A time bin: A time interval used to place observations in a reporting group. Its endpoints, included boundaries and label are part of the metric definition. has a boundary rule and a label, both chosen before the sum.
The examples use one financial year of synthetic sales from a college canteen that is closed on Sundays, so 52 days of the year have no row at all.
"""Daily canteen sales for one financial year, April to March, closed on Sundays (shared by the examples)."""
import numpy as np
import pandas as pd
def canteen_sales():
days = pd.date_range("2025-04-01", "2026-03-31", freq="D")
days = days[days.dayofweek != 6] # closed on Sundays: those days have no row at all
rng = np.random.default_rng(26)
return pd.Series(rng.integers(2800, 5200, len(days)), index=days, name="sales") """One financial year of daily sales, resampled to weeks, months and financial-year quarters."""
from canteen import canteen_sales
sales = canteen_sales()
print(f"{len(sales)} days with sales, from {sales.index[0]:%a %d %b %Y} to {sales.index[-1]:%a %d %b %Y}")
weeks = sales.resample("W").agg(["sum", "count"])
print("\nFirst weeks ('W': weeks end on Sunday and are labelled with it):")
print(weeks.head(3).to_string())
months = sales.resample("ME").sum()
print("\nMonths ('ME': labelled with the last day of the month):")
print(months.head(4).to_string())
quarters = sales.resample("QE-MAR").sum()
fy = quarters.index.to_period("Q-MAR")
print("\nQuarters of the financial year ('QE-MAR'):")
for end, period, total in zip(quarters.index, fy, quarters):
print(f"{end:%Y-%m-%d} {period} FY{period.qyear - 1}-{period.qyear % 100:02d} Q{period.quarter} {total:,}")
print("\nYear total:", f"{sales.sum():,}", "| quarters add up:", quarters.sum() == sales.sum()) Output
313 days with sales, from Tue 01 Apr 2025 to Tue 31 Mar 2026
First weeks ('W': weeks end on Sunday and are labelled with it):
sum count
2025-04-06 20079 5
2025-04-13 24486 6
2025-04-20 26152 6
Months ('ME': labelled with the last day of the month):
2025-04-30 106435
2025-05-31 110839
2025-06-30 104841
2025-07-31 108984
Freq: ME
Quarters of the financial year ('QE-MAR'):
2025-06-30 2026Q1 FY2025-26 Q1 322,115
2025-09-30 2026Q2 FY2025-26 Q2 317,095
2025-12-31 2026Q3 FY2025-26 Q3 312,439
2026-03-31 2026Q4 FY2025-26 Q4 317,522
Year total: 1,269,171 | quarters add up: True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 resample_canteen.py
"W" makes weeks that end on Sunday and labels each with that Sunday. The first week holds 5 days, not 6, because the
financial year starts on a Tuesday. Calendar rules choose the bin boundaries; count measures the usable
observations supplied within them.
"ME" labels each month with its last day, and "MS" would label the same months with their first day. The four
quarters of "QE-MAR" end in June, September, December and March, and their totals add up to the year, a check worth
keeping in every report. resample needs a datetime index, or a datetime column named with on="date"; on a plain
numbered index it raises a TypeError. After the rule, any aggregation that a group-by accepts works, including agg
with a list of functions.
Equal quarter ends, explicit calendar and fiscal Period labels
Text description of the diagram
Four cards retain the full April-to-March structure in two columns. April–June is calendar 2025Q2 and fiscal 2026Q1; July–September is calendar 2025Q3 and fiscal 2026Q2; October–December is calendar 2025Q4 and fiscal 2026Q3; January–March is calendar 2026Q1 and fiscal 2026Q4. All four fiscal spans belong to FY2025-26. QE and QE-MAR resampling use these same quarter-end dates and sums; the displayed period conventions, not the resample alone, supply the different numbering.
Look at the first weekly count and the four quarter totals in this walk-through:
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Keep the original grain. The synthetic canteen has 313 daily sales observations. Sunday is a known closed day in this scenario, so it contributes no source row.
- Keep calendar coverage visible. The first weekly bin has only five observed open days because the input starts on Tuesday. The bin label does not make this a full Monday-to-Sunday week.
- Reconcile all four sums. The financial-year quarter totals are 322,115, 317,095, 312,439 and 317,522. They add to 1,269,171, the same sales total as the source.
Key idea
Checkpoint
Check: Time bins
Use offset aliases for time-point bins
pandas 3 enforced a rename of several frequencies that pandas 2 had deprecated. Old code fails loudly, which is better than failing quietly:
"""The frequency strings pandas 3 removed, what it says, and what to write instead."""
import re
import pandas as pd
from canteen import canteen_sales
sales = canteen_sales()
def reason(error):
"""The useful part of pandas' message."""
found = re.search(r"'[\w-]+' is no longer supported for offsets\. Please use '[\w-]+' instead\.|Did you mean [\w-]+\?|for Period, please use '[\w-]+' instead of '[\w-]+'", str(error))
return found.group(0) if found else str(error)
print("resample() takes offset aliases:")
for old, new in [("M", "ME"), ("Q", "QE"), ("Y", "YE"), ("H", "h"), ("Q-MAR", "QE-MAR")]:
try:
sales.resample(old).sum()
except ValueError as error:
print(f" {old!r:9} ValueError: {reason(error)}")
print(f" {new!r:9} {len(sales.resample(new).sum())} bins")
print("\nPeriods take period aliases:")
day = sales.index[200]
for freq in ["QE-MAR", "Q-MAR"]:
try:
print(f" {freq!r:9} {pd.Period(day, freq=freq)}")
except ValueError as error:
print(f" {freq!r:9} ValueError: {reason(error)}") Output
resample() takes offset aliases: 'M' ValueError: 'M' is no longer supported for offsets. Please use 'ME' instead. 'ME' 12 bins 'Q' ValueError: 'Q' is no longer supported for offsets. Please use 'QE' instead. 'QE' 4 bins 'Y' ValueError: 'Y' is no longer supported for offsets. Please use 'YE' instead. 'YE' 2 bins 'H' ValueError: Did you mean h? 'h' 8737 bins 'Q-MAR' ValueError: 'Q-MAR' is no longer supported for offsets. Please use 'QE-MAR' instead. 'QE-MAR' 4 bins Periods take period aliases: 'QE-MAR' ValueError: for Period, please use 'Q-MAR' instead of 'QE-MAR' 'Q-MAR' 2026Q3
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 old_aliases.py
| Criterion | Offsets: resample, date_range | Periods: Period, to_period |
|---|---|---|
| Month | ME (month end) or MS (month start); M is refused | M |
| Quarter | QE or QS; Q is refused | Q |
| Financial-year quarter | QE-MAR; Q-MAR is refused | Q-MAR; QE-MAR is refused |
| Year | YE or YS; Y and A are refused | Y, or Y-MAR for a year ending in March |
| Hour, minute, second | h, min, s; H, T and S are refused | h, min, s |
The two columns are the trap. Offsets mark points on the time line, so they say whether a bin is labelled by its end
(E) or its start (S). Periods are spans of time with no such choice, so they keep the short names. A
financial-year quarter is therefore "QE-MAR" in resample and "Q-MAR" in to_period, and each function refuses
the other’s spelling with a message that names the right one.
Walk through the documented change and the current native behavior; keep the span notation separate:
- pandas 2.2
Offset end aliases deprecated
pandas 2.2 deprecated M, Q and Y for offset rules in favor of ME, QE and YE, and uppercase hour aliases in favor of h.
- pandas 3
Current native calls reject old aliases
old_aliases.py runs on the pinned pandas 3. M, Q, Y, H and Q-MAR raise ValueError; their replacement offset strings run.
- Period spans
Use the separate span notation
The current Period call accepts Q-MAR and rejects QE-MAR. A Period labels a span; an offset string creates time points or reporting bins.
The timeline has 3 events, from pandas 2.2 to Period spans.
Key idea
Checkpoint
Check: Frequency aliases
Number fiscal quarters from April
Reports by Indian financial years name the year by both calendar years it touches, such as FY2025-26, and number its quarters from April: Q1 is April to June, and Q4 is January to March. The Reserve Bank of India labels quarters the same way in its policy statements, for example Q1:2027-28 for the first quarter of the 2027-28 year.
pandas has the building blocks but its own names. to_period("Q-MAR") turns each day into a quarter period, and the
period’s qyear is the calendar year in which its financial year ends, so every quarter of FY2025-26 is labelled
2026, from 2026Q1 to 2026Q4. Its quarter is already the financial-year quarter. The label a finance team reads
is one f-string away: f"FY{p.qyear - 1}-{p.qyear % 100:02d} Q{p.quarter}". Build labels from qyear, never from the
calendar year of the date, which gives January to March the wrong year. Labels in this form also sort correctly as
text, because the starting year comes first. Timestamps stored in UTC need converting to IST before the period is
taken: an order placed at 20:00 UTC on the last day of March is already in April on the IST clock, and so in the next
financial year.
The quarter-end dates of plain QE and QE-MAR coincide, because December and March anchors share the same
three-month boundaries. Their fiscal numbering differs: use Q-MAR periods for the ending year and quarter,
rather than treating an end-date label as a financial-year label. Compare both representations in this run:
"""The same canteen bin boundaries, with explicit frequency and Period labels."""
import pandas as pd
from canteen import canteen_sales
sales = canteen_sales()
calendar = sales.resample("QE").sum()
fiscal = sales.resample("QE-MAR").sum()
print("Values and quarter-end timestamps match:", calendar.equals(fiscal))
print("Frequency metadata:", calendar.index.freqstr, "and", fiscal.index.freqstr)
print("\nThe same quarter ends labelled as calendar and fiscal periods:")
for end, total in fiscal.items():
print(f"{end:%Y-%m-%d}: {total:,} | calendar {end.to_period('Q')} | fiscal {end.to_period('Q-MAR')}")
print("\nPeriod aliases are a separate vocabulary:")
for alias in ["M", "ME", "Q-MAR", "QE-MAR", "Y-MAR", "YE-MAR"]:
try:
print(alias, "->", pd.Period(sales.index[0], freq=alias))
except ValueError as error:
print(alias, "->", type(error).__name__, str(error).split(".")[0]) Output
Values and quarter-end timestamps match: True Frequency metadata: QE-DEC and QE-MAR The same quarter ends labelled as calendar and fiscal periods: 2025-06-30: 322,115 | calendar 2025Q2 | fiscal 2026Q1 2025-09-30: 317,095 | calendar 2025Q3 | fiscal 2026Q2 2025-12-31: 312,439 | calendar 2025Q4 | fiscal 2026Q3 2026-03-31: 317,522 | calendar 2026Q1 | fiscal 2026Q4 Period aliases are a separate vocabulary: M -> 2025-04 ME -> ValueError Invalid frequency: ME Q-MAR -> 2026Q1 QE-MAR -> ValueError Invalid frequency: QE-MAR Y-MAR -> 2026 YE-MAR -> ValueError Invalid frequency: YE-MAR
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 quarter_boundaries.py
Follow the same reporting dates through fiscal numbering, then compare the Period labels.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Form the date on its clock. The canteen's date-only rows already use the reporting calendar. For UTC event timestamps, convert to IST before taking a date or a quarter.
- Start numbering in April. April to June is Q1, July to September Q2, October to December Q3 and January to March Q4. A calendar quarter number alone gives a different numbering.
- Name the same fiscal span. Every Q-MAR period in the financial year shares its ending-year qyear. The run shows that identical quarter-end timestamps can receive calendar or financial-year Period labels.
Key idea
Checkpoint
Check: Financial-year quarters
Bin edges in depth: weeks, business days and clock changes Pro: opens with a Pro, Premium or Ultimate pass
Every bin has two edges, and pandas has to decide which edge names the bin and which edge belongs to it. These decisions move rows between weeks and days, so this part works them out on numbers you can check.
In this part:
- Choose a week’s edges before naming it
- Move a business-day start with an elapsed rule
- Keep civil bins separate from elapsed bins
3 diagrams · 2 runnable examples (Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Financial-year reports: quarters, halves and year to date, with numbers Pro: opens with a Pro, Premium or Ultimate pass
Finance teams read quarters, half-years and year-to-date totals of the financial year. This part builds all three from 24 months of revenue, then reproduces two mistakes that produce plausible but wrong totals.
In this part:
- Reconcile quarters and year-to-date totals
- Use the financial-year ending year for labels
- Derive halves from quarters when data starts late
3 diagrams · 4 runnable examples (CSV, Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Gaps that resampling exposes: closed, missing or zero Pro: opens with a Pro, Premium or Ultimate pass
A day without rows can mean that the shop was closed, that the data did not arrive, or that nothing was sold, and resampling turns all three into the same zero. This part tells them apart and shows what each aggregation returns for an empty bin.
In this part:
- Keep missing coverage distinct from a real zero
- Spread totals without inventing daily observations
2 diagrams · 2 runnable examples (Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise financial-year totals
The exercise builds the same labels with the standard library, which also runs in the browser.
Exercise · Easy · Python
Label financial-year quarters and total the revenue
Finance reports by Indian financial year, which runs from April to March: FY2026-27 starts on 2026-04-01 and ends on 2027-03-31, Q1 is April to June and Q4 is January to March. Write two functions in fy_quarters.py:
fy_quarter(day): the label of the quarter adatetime.datefalls in, such as"FY2026-27 Q3". The second year always has two digits, so the year that starts in 2099 is"FY2099-00".revenue_by_fy_quarter(rows):rowsis a list of(day, amount)pairs, with the day as text inYYYY-MM-DDform. Return a dictionary from label to total, in calendar order from the first quarter with a sale to the last, and give every quarter in between a total of 0, the wayresample("QE-MAR").sum()does.
The rows can come in any order, and there is at least one.
Starter code · fy_quarters.py
from datetime import date
def fy_quarter(day):
"""The financial-year quarter of a date, such as "FY2026-27 Q3" for date(2026, 10, 4)."""
# Replace this line with your code.
return ""
def revenue_by_fy_quarter(rows):
"""{"FY2025-26 Q4": 2500, "FY2026-27 Q1": 1200, ...} in calendar order, with 0 for quarters without sales.
rows is a list of (day, amount) pairs, such as ("2026-04-02", 1200).
"""
# Replace this line with your code.
return {} The sample tests · test_fy_quarters.py
from datetime import date
from fy_quarters import fy_quarter, revenue_by_fy_quarter
def test_quarter_labels():
"""labels the quarters of a financial year that starts in April"""
assert fy_quarter(date(2026, 4, 1)) == "FY2026-27 Q1"
assert fy_quarter(date(2026, 10, 4)) == "FY2026-27 Q3"
assert fy_quarter(date(2027, 3, 31)) == "FY2026-27 Q4"
def test_january_to_march():
"""puts January to March in the financial year that started the April before"""
assert fy_quarter(date(2026, 3, 31)) == "FY2025-26 Q4"
assert fy_quarter(date(2026, 1, 1)) == "FY2025-26 Q4"
def test_two_digit_end_year():
"""writes the end year with two digits, also at the turn of a century"""
assert fy_quarter(date(2099, 12, 1)) == "FY2099-00 Q3"
def test_totals_across_a_year_boundary():
"""adds up each quarter, across the start of a new financial year, in calendar order"""
rows = [("2026-04-02", 1200), ("2026-02-27", 900), ("2026-03-31", 1600), ("2026-06-30", 300)]
result = revenue_by_fy_quarter(rows)
assert result == {"FY2025-26 Q4": 2500, "FY2026-27 Q1": 1500}
assert list(result) == ["FY2025-26 Q4", "FY2026-27 Q1"]
def test_empty_quarters_are_zero():
"""gives a quarter without sales a total of 0, as resample does"""
rows = [("2026-05-10", 400), ("2027-01-15", 250)]
assert list(revenue_by_fy_quarter(rows).items()) == [
("FY2026-27 Q1", 400),
("FY2026-27 Q2", 0),
("FY2026-27 Q3", 0),
("FY2026-27 Q4", 250),
] A hint
The financial year starts in the calendar year of the date when the month is April or later, and in the year before otherwise. Counting months from April, (day.month - 4) % 12 gives 0 for April and 11 for March, so dividing it by 3 gives the quarter minus one. For the totals, number each quarter as start_year * 4 + quarter - 1: consecutive quarters get consecutive numbers, which makes it easy to walk from the first to the last.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Practice that adapts to you
Practise this lesson
6 questions about 11 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
-
Fresher to mid · Warm-up
Why does
resample("M")fail in pandas 3, and what should you write instead?pandas 3 enforced a rename that pandas 2 had announced with deprecation warnings: the offset aliases for month, quarter and year ends gained an E, so
"M"became"ME","Q"became"QE"and"Y"became"YE", and the hour is now a lower-case"h". The old strings raise aValueErrorwhose message names the replacement. Write"ME"for months labelled by their last day or"MS"for months labelled by their first day; for Indian financial-year quarters write"QE-MAR". Periods are the exception:Periodandto_periodstill take"M","Q"and"Q-MAR", because a period is a span with no end or start to choose, and they refuse theEforms. -
Fresher to mid
Finance wants revenue for each quarter of FY 2026-27, labelled FY2026-27 Q1 to Q4, from a table of orders whose timestamps are stored in UTC. Walk through your pandas steps and the checks you would add before sending the table.
-
Mid
Weekly active users on a dashboard are labelled with each Sunday, but the business counts weeks from Monday. Two analysts using resample get different numbers for the same week. How do you find out why, and make them agree?
-
Mid to senior
A monthly revenue chart built with resample shows 0 for a month in which the order feed was down throughout. Leadership asks why revenue collapsed. Explain what happened and how you would change the pipeline.
Key takeaways
resample(rule)groups rows into bins of time and aggregates each bin; the bins cover the calendar, so count the rows in each.- pandas 3 refuses
M,Q,YandH: writeMEorMS,QEorQS,YEorYS, andh. - Offsets end in
EorS(QE-MARforresample); periods do not (Q-MARforto_period). - An Indian financial year runs from April to March; build its labels from
qyear, which names the year in which it ends.
Terms in this lesson
- resampling
- Grouping time-indexed observations into new time bins, or introducing new timestamps when changing frequency; the labels and aggregation define the result.
- time bin
- A time interval used to place observations in a reporting group. Its endpoints, included boundaries and label are part of the metric definition.
References
- Time series / date functionality: resampling (pandas user guide) (pandas development team (NumFOCUS))
- Time series / date functionality: offset aliases (pandas user guide) (pandas development team (NumFOCUS))
- pandas.DataFrame.resample (pandas development team (NumFOCUS))
- pandas.Period (pandas development team (NumFOCUS))
- What's new in 3.0.0 (pandas) (pandas development team (NumFOCUS))
- Monetary Policy Statement, 2026-27: Resolution of the Monetary Policy Committee (Reserve Bank of India)
- The Reserve Bank's Accounts for 2025-26 (Annual Report) (Reserve Bank of India)
- pandas.Series.resample (pandas development team (NumFOCUS))
- pandas.Period.qyear (pandas development team (NumFOCUS))
- Frequency deprecations in pandas 2.2 (pandas development team (NumFOCUS))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress