Dates & Time Series

Data Science · Lesson 16 of 32 6 min read Updated

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).

PythonRun it yourself
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"))
Output
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.

PythonRun it yourself
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))
Output
  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.

PythonRun it yourself
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))
Output
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:

AliasBucketLabelled by
"D"Daythe day
"W"Weekthe Sunday that ends it
"ME"Monthmonth end
"QE"Quarterquarter end
"YE"Yearyear end
"h", "min"Hour, minutethe start
PythonRun it yourself
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)
Output
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.0

The 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.

PythonRun it yourself
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()
Output
            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.5

window=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 .dt parts. Convert with parse_dates or pd.to_datetime().
  • Ambiguous day and month. 05/11/2025 is 5 November in India but 11 May in the US. Always give format for 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 (or on="column"). Sort the index when you slice by date.

Key points

  • pd.to_datetime() converts text to dates; give format for day-first dates and errors="coerce" for bad values.
  • The .dt accessor provides year, month, month_name(), day_name(), dayofweek and more.
  • With a DatetimeIndex, df.loc["2025-10"] selects a whole month and date slices include both ends.
  • resample("W"), resample("ME") and resample("QE") aggregate by time buckets.
  • rolling(7).mean() smooths noise; shift(), diff() and pct_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
Python
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))
Output
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?

2. What does df.resample("ME").sum() do?

3. In pandas 3.0, what happens with resample("M")?

4. What does s.rolling(7).mean() produce?

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