pandas·Lesson 21·12 min·0/4 exercises

Dates and times

A date read from a file is a string, and strings sort in the wrong order.

pd.to_datetime.dt accessorDatetimeIndexpartial string indexingTimedelta

Watch it happen

Play it through, or step back and forth yourself.

df
date
str
cups
int64
0
2026-03-02
120
1
2026-03-10
95
2
2026-03-3
150
3
2026-03-21
80

A date read from a CSV is a str until you say otherwise. It looks like a date and prints like a date, and nothing warns you.

The idea

orders["date"] looks like a date, prints like a date, and is a str. Nothing warns you, and everything works — badly.

Why it matters

Strings sort lexicographically, character by character. So "2026-03-3" sorts after "2026-03-21", because "3" comes after "2". One missing zero and your chronology is silently wrong.

You also can't subtract them, can't ask which weekday they fall on, and can't resample by month. All of that needs real timestamps.

Parsing

orders["date"] = pd.to_datetime(orders["date"])
orders["date"].dtype        # datetime64[ns]

Better still, do it on the way in:

pd.read_csv("orders.csv", parse_dates=["date"])

For input that fights back:

pd.to_datetime(s, format="%d/%m/%Y")   # you know the layout — faster, fails loudly
pd.to_datetime(s, dayfirst=True)       # 05/03 is 5 March, not 3 May
pd.to_datetime(s, errors="coerce")     # unparseable values become NaT

Passing format= is worth the keystrokes on a large file — it's much faster than letting pandas guess per value, and a value that doesn't match raises instead of being quietly reinterpreted.

NaT — "not a time" — is the datetime equivalent of NaN, and .isna() finds it just the same.

The .dt accessor

Date parts live behind .dt, exactly as string methods live behind .str:

orders["date"].dt.year
orders["date"].dt.month
orders["date"].dt.day_name()      # "Monday"
orders["date"].dt.dayofweek       # 0 = Monday
orders["date"].dt.quarter
orders["date"].dt.to_period("M")  # 2026-03

Each is an ordinary column, so they group like anything else — orders.groupby(orders["date"].dt.day_name())["cups"].mean() gives you the weekly pattern in one line.

Dates as the index

This is the move that makes time series work pleasant:

ts = orders.set_index("date").sort_index()

ts.loc["2026-03"]                    # the whole of March
ts.loc["2026"]                       # the whole year
ts.loc["2026-03-02":"2026-03-05"]    # a range — end INCLUDED

That's partial string indexing: with a DatetimeIndex, pandas understands "2026-03" as a period rather than a label, and gives you everything inside it. It's why nearly every time series recipe starts by putting the date in the index.

Sort it. Unsorted date indexes give wrong slices and warnings, and .sort_index() is one call.

Durations

orders["date"].max() - orders["date"].min()      # Timedelta("5 days")
(pd.Timestamp("2026-04-01") - orders["date"]).dt.days
orders["date"] + pd.Timedelta(days=7)
orders["date"] + pd.DateOffset(months=1)         # calendar-aware

Timedelta is a fixed span — seven days is always 168 hours. DateOffset is calendar-aware: "one month" from 31 January lands on 28 February. Use the second whenever a human would say "next month".

See it run

The lesson's code, ready to run and to fiddle with.

Putting the kettle on…

Starting up…

Worked example

not graded

Already written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.

trygrouping by dates.dt.day_name() to see the weekly pattern.

Press Run — the output appears here.

Your turn

4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.

Convert orders["date"] to real timestamps and return the resulting dtype.

your answer

Return the weekday name for every row of orders.

your answer

Return the total cups per weekday name — group by the parsed date's day name.

your answer

How long does orders span? Return max - min of the parsed dates.

your answer