Dates & Time Series
Sales by month, temperature by day, website visits by hour: a huge share of real data is a time series, a sequence of values recorded over time. To analyse it you first need real date values, not text that merely looks like dates.
In this lesson you will convert text to dates, pull out parts such as the month and weekday, filter by date ranges, and use two of pandas' most powerful time tools: resample() to change the time scale and rolling() to smooth noisy data.
Converting text to dates
pd.to_datetime() turns text into datetime values. ISO dates such as 2025-10-20 are read without help. For other layouts, give a format using codes like %d (day), %m (month number), %b (short month name) and %Y (four-digit year).
import pandas as pd
iso = pd.to_datetime(pd.Series(["2025-10-20", "2025-11-05"]))
print(iso)
indian = pd.to_datetime(pd.Series(["05/11/2025", "20/10/2025"]), format="%d/%m/%Y")
print(indian.dt.strftime("%d %B %Y").tolist())
messy = pd.to_datetime(pd.Series(["2025-10-20", "not known", "2025-13-01"]), errors="coerce")
print(messy)
print(pd.Timestamp("2025-10-20") - pd.Timestamp("2025-08-15"))0 2025-10-20 1 2025-11-05 dtype: datetime64[us] ['05 November 2025', '20 October 2025'] 0 2025-10-20 1 NaT 2 NaT dtype: datetime64[us] 66 days 00:00:00
Each date's dtype is datetime64[us], meaning it is stored with microsecond precision. With format="%d/%m/%Y", 05/11/2025 is correctly read as 5 November, not 11 May. errors="coerce" turns anything that is not a valid date, including the impossible month 13, into NaT (not a time). Subtracting two dates gives a Timedelta, a length of time.
Date parts with the .dt accessor
Once a column holds datetimes, the .dt accessor gives you its parts. These become new columns you can group and filter by.
import pandas as pd
sales = pd.read_csv("sales.csv", parse_dates=["order_date"]).drop_duplicates()
d = sales["order_date"]
sales["month"] = d.dt.month
sales["month_name"] = d.dt.month_name()
sales["weekday"] = d.dt.day_name()
sales["quarter"] = d.dt.quarter
sales["is_weekend"] = d.dt.dayofweek >= 5 # Monday = 0, Sunday = 6
print(sales[["order_date", "month", "month_name", "weekday", "quarter", "is_weekend"]].head(4))
print(sales["weekday"].value_counts())
print("Weekend share:", round(sales["is_weekend"].mean(), 3))order_date month month_name weekday quarter is_weekend 0 2025-01-01 1 January Wednesday 1 False 1 2025-01-01 1 January Wednesday 1 False 2 2025-01-01 1 January Wednesday 1 False 3 2025-01-02 1 January Thursday 1 False weekday Saturday 228 Sunday 200 Wednesday 174 Thursday 163 Monday 162 Tuesday 151 Friday 131 Name: count, dtype: int64 Weekend share: 0.354
Saturdays and Sundays together have more orders than you would expect from 2 days out of 7: the weekend share is about a third. Note dayofweek, where Monday is 0 and Sunday is 6.
Filtering by date and the DatetimeIndex
You can compare date columns with date strings directly. If you make the date the index, you can also select with partial strings such as "2025-10" for a whole month.
import pandas as pd
weather = pd.read_csv("weather.csv", parse_dates=["date"])
mumbai = weather[weather["city"] == "Mumbai"].set_index("date").sort_index()
monsoon = mumbai.loc["2025-06-01":"2025-09-30"]
print(len(monsoon), "monsoon days, total rain", round(monsoon["rainfall_mm"].sum()), "mm")
july = mumbai.loc["2025-07"]
print(july["rainfall_mm"].describe().round(1))
print(mumbai.loc[mumbai.index.dayofweek == 6, "temp_max"].head(3))122 monsoon days, total rain 1649 mm count 31.0 mean 16.0 std 17.0 min 0.0 25% 0.4 50% 12.1 75% 27.0 max 57.4 Name: rainfall_mm, dtype: float64 date 2025-01-05 30.8 2025-01-12 32.2 2025-01-19 32.0 Name: temp_max, dtype: float64
mumbai.loc["2025-07"] selects every row in July without you needing to know how many days it has. Slices of date strings include both ends. The index has the same date properties as .dt, such as index.dayofweek.
resample(): changing the time scale
resample() is like groupby() for time: it puts rows into time buckets (weeks, months, quarters) and then aggregates each bucket. The bucket size is given by a frequency alias:
| Alias | Bucket | Labelled by |
|---|---|---|
"D" | Day | the day |
"W" | Week | the Sunday that ends it |
"ME" | Month | month end |
"QE" | Quarter | quarter end |
"YE" | Year | year end |
"h", "min" | Hour, minute | the start |
import pandas as pd
sales = pd.read_csv("sales.csv", parse_dates=["order_date"]).drop_duplicates()
sales["revenue"] = sales["quantity"] * sales["unit_price"] * (1 - sales["discount"] / 100)
daily = sales.set_index("order_date")["revenue"]
monthly = daily.resample("ME").sum().round()
print(monthly)
quarterly = daily.resample("QE").agg(["count", "sum"]).round()
print(quarterly)order_date
2025-01-31 112754.0
2025-02-28 117006.0
2025-03-31 147515.0
2025-04-30 114287.0
2025-05-31 109602.0
2025-06-30 95327.0
2025-07-31 168402.0
2025-08-31 161299.0
2025-09-30 193068.0
2025-10-31 280677.0
2025-11-30 274834.0
2025-12-31 132068.0
Freq: ME, Name: revenue, dtype: float64
count sum
order_date
2025-03-31 261 377276.0
2025-06-30 228 319216.0
2025-09-30 309 522769.0
2025-12-31 405 687580.0The month-end labels (2025-01-31 and so on) are how pandas names each bucket. October is the clear peak, more than double the quiet months of May and June. agg() returns several statistics per bucket: the fourth quarter has both the most orders and the most revenue.
rolling(): smoothing noisy data
Daily values jump around. A rolling mean (moving average) replaces each day with the average of a window of days around it, which shows the trend. shift() moves values down by a number of rows, which lets you compare each day with an earlier one, and pct_change() gives the percentage change.
import pandas as pd
import matplotlib.pyplot as plt
weather = pd.read_csv("weather.csv", parse_dates=["date"])
delhi = weather[weather["city"] == "Delhi"].set_index("date")["temp_max"]
smooth = delhi.rolling(window=7, center=True, min_periods=4).mean()
print(pd.DataFrame({"daily": delhi, "7-day mean": smooth.round(1)}).loc["2025-05-10":"2025-05-14"])
monthly = delhi.resample("ME").mean().round(1)
print(pd.DataFrame({"mean": monthly, "change": monthly.diff(), "pct": (monthly.pct_change() * 100).round(1)}).head(4))
plt.plot(delhi.index, delhi, color="lightgray", label="Daily maximum")
plt.plot(smooth.index, smooth, color="crimson", label="7-day mean")
plt.title("Delhi maximum temperature, 2025")
plt.ylabel("°C")
plt.legend()
plt.show() daily 7-day mean
date
2025-05-10 39.5 39.8
2025-05-11 41.3 39.6
2025-05-12 42.5 39.7
2025-05-13 40.9 40.3
2025-05-14 37.4 40.7
mean change pct
date
2025-01-31 20.0 NaN NaN
2025-02-28 24.5 4.5 22.5
2025-03-31 29.7 5.2 21.2
2025-04-30 35.8 6.1 20.5window=7 averages 7 days, center=True puts the window around each day instead of before it, and min_periods=4 still gives a value near the start and end of the year, where fewer than 7 days are available. In the chart, the grey daily line is jagged, while the red 7-day line clearly rises from about 20 °C in January to a peak above 40 °C in May and June, dips during the monsoon, and falls again through November and December. diff() and pct_change() show the month-to-month change: February was 4.5 °C (22.5%) warmer than January, and each month until April was warmer than the one before by 5 to 6 °C.
Common mistakes
- Leaving dates as text. Text dates sort correctly only in ISO format and have no
.dtparts. Convert withparse_datesorpd.to_datetime(). - Ambiguous day and month.
05/11/2025is 5 November in India but 11 May in the US. Always giveformatfor non-ISO dates. - Using removed aliases.
"M","Q","Y"and"H"fail in pandas 3.0; write"ME","QE","YE"and"h". - Resampling an unsorted or non-date index.
resample()needs a DatetimeIndex (oron="column"). Sort the index when you slice by date.
Key points
pd.to_datetime()converts text to dates; giveformatfor day-first dates anderrors="coerce"for bad values.- The
.dtaccessor providesyear,month,month_name(),day_name(),dayofweekand more. - With a DatetimeIndex,
df.loc["2025-10"]selects a whole month and date slices include both ends. resample("W"),resample("ME")andresample("QE")aggregate by time buckets.rolling(7).mean()smooths noise;shift(),diff()andpct_change()compare with earlier periods.
Exercise
Using sales.csv (drop duplicates first), count the number of orders per week with resample("W"). Print the three busiest weeks. Then compute a 4-week rolling mean of the weekly order count and print its highest value, rounded to one decimal.
Show solution
import pandas as pd
sales = pd.read_csv("sales.csv", parse_dates=["order_date"]).drop_duplicates()
weekly = sales.set_index("order_date")["order_id"].resample("W").count()
print(weekly.nlargest(3))
print(round(weekly.rolling(4).mean().max(), 1))order_date 2025-11-02 41 2025-10-12 37 2025-10-26 37 Name: order_id, dtype: int64 37.8
count() counts the orders in each weekly bucket. All three busiest weeks fall in October and early November, the festive season, and the rolling mean confirms that this was a sustained rise rather than one lucky week.
Quick Quiz
1. Which code reads "25/12/2025" correctly as Christmas Day?
%d is the day and %m the month, in the order they appear in the text.
2. What does df.resample("ME").sum() do?
resample groups rows into time buckets; "ME" means month-end buckets.
3. In pandas 3.0, what happens with resample("M")?
The old "M" alias was removed; use "ME" (or "MS" for month start).
4. What does s.rolling(7).mean() produce?
A rolling window moves along the data, averaging the current row and the 6 before it.
Learn Data Science with a mentor
Free lessons get you started. Our Data Science & Machine Learning training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
