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 indexingTimedeltaWatch it happen
Play it through, or step back and forth yourself.
dfA 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 NaTPassing 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-03Each 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 INCLUDEDThat'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-awareTimedelta 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 gradedAlready 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.
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.
Return the weekday name for every row of orders.
Return the total cups per weekday name — group by the parsed date's day name.
How long does orders span? Return max - min of the parsed dates.
