How do I round timestamps to the nearest hour or day in pandas?
Short answer
Use dt.floor to round down, dt.ceil to round up and dt.round to go to the nearest. Floor is usually what you want for bucketing, because round can push a timestamp into the next period.
The short version
dt.floor("h") rounds down to the hour, dt.ceil("h") rounds up, and dt.round("h") goes to whichever is nearer. For grouping events into time buckets, use floor, because round can push a timestamp forward into a period it did not belong to.
The three in action
import pandas as pd
t = pd.Series(pd.to_datetime(["2021-01-01 10:37"]))
t.dt.floor("h") # 2021-01-01 10:00, rounded down
t.dt.round("h") # 2021-01-01 11:00, nearest
t.dt.ceil("h") # 2021-01-01 11:00, rounded up
Note that round sent 10:37 forward to 11:00, because it is nearer. If you were bucketing events by hour, that event would be counted in the 11 o'clock hour despite happening at 10:37, which is wrong.
Why floor is the right default for bucketing
An event at 10:37 happened during the ten o'clock hour. Flooring assigns it there, which is what "which hour did this happen in" means. Rounding assigns events to whichever hour is closer, which splits a single hour's events across two buckets and inflates one at the expense of the other.
So for grouping by time period, always floor.
df["hour"] = df["timestamp"].dt.floor("h")
df.groupby("hour").size()
Common frequencies
The same codes work throughout: "h" hour, "D" day, "min" minute, "15min" for a quarter-hour bucket, "s" second.
df["timestamp"].dt.floor("15min") # quarter-hour buckets
df["timestamp"].dt.floor("D") # midnight of the same day
Flooring to "D" is the standard way to strip the time and keep only the date, while remaining a real datetime you can sort and compare.
Just the date
If you want a plain date rather than a datetime at midnight, dt.date gives one, but note it produces Python date objects, which lose the fast datetime operations.
df["timestamp"].dt.date # date objects, slower for further work
df["timestamp"].dt.floor("D") # still a datetime, keeps dt functionality
Prefer floor("D") when you will do more date work, and dt.date only for final display.
Rounding and time zones
On an aware timestamp, flooring operates on the local wall-clock time, which is usually what you want ("which local hour did this fall in"). But around a clock change, local hours are not all one hour long, so a floored bucket can contain more or fewer minutes than you expect. If exact durations matter, floor in UTC and convert afterwards.
Rounding halves
dt.round uses the same round-half-to-even convention as Python's round, so a timestamp exactly on a half hour rounds to the even hour. This is rarely important, but it explains an occasional surprise where a timestamp rounds "the wrong way".