Chapter 17 Dates and Basic Time-Series Operations

Dates require explicit types and ordering. String dates may sort incorrectly, and time-based calculations are unreliable when rows are unsorted or time zones are mixed.

17.1 Create and parse dates

import pandas as pd

daily_index = pd.date_range(start="2026-01-01", periods=4, freq="D")

assert daily_index[0] == pd.Timestamp("2026-01-01")
assert daily_index.freqstr == "D"

Use an explicit format for known input formats:

raw_dates = pd.Series(["07/01/2026", "07/15/2026"])
parsed_dates = pd.to_datetime(raw_dates, format="%m/%d/%Y")

assert parsed_dates.dt.month.tolist() == [7, 7]
assert parsed_dates.dt.day.tolist() == [1, 15]

17.2 Sort before calculating changes

diff() computes the change from the previous row in the current order. Sort by entity and time before using it on panel data.

revenue = pd.DataFrame(
    {
        "date": pd.to_datetime(["2026-01-01", "2026-01-02", "2026-01-03"]),
        "revenue": [100.0, 120.0, 90.0],
    }
).sort_values("date")

revenue["daily_change"] = revenue["revenue"].diff()
revenue["growth_rate"] = revenue["revenue"].pct_change()

assert pd.isna(revenue.loc[0, "daily_change"])
assert revenue.loc[1, "daily_change"] == 20.0
assert round(revenue.loc[1, "growth_rate"], 2) == 0.20

The first difference is missing because no prior observation exists. Decide whether to retain that missing value or define a domain-specific baseline; do not automatically replace it with zero.

17.3 Grouped differences

For multiple entities, calculate changes within each entity rather than across adjacent rows belonging to different groups.

panel = pd.DataFrame(
    {
        "customer_id": [1, 1, 2, 2],
        "date": pd.to_datetime(["2026-01-01", "2026-02-01"] * 2),
        "spend": [10, 15, 20, 18],
    }
).sort_values(["customer_id", "date"])

panel["spend_change"] = panel.groupby("customer_id")["spend"].diff()

assert panel["spend_change"].tolist()[1] == 5
assert panel["spend_change"].tolist()[3] == -2

17.4 Resampling requires a time index

daily = pd.Series(
    [10, 20, 30, 40],
    index=pd.date_range("2026-01-01", periods=4, freq="D"),
)
two_day_totals = daily.resample("2D").sum()

assert two_day_totals.tolist() == [30, 70]

Aggregation frequency changes the unit of analysis. State whether a result is a daily sum, monthly average, end-of-period value, or another defined measure.

17.5 Interview checklist

  • Are values true datetimes or only strings?
  • Are entity and time ordering explicit?
  • Should differences be calculated within groups?
  • What does the first missing difference mean?
  • Does resampling use the correct aggregation and frequency?
  • Could future information enter a feature through a centered or forward-looking window?

17.6 Exercises

  1. Calculate month-over-month revenue change for two products.
  2. Explain why applying diff() before sorting can produce plausible but wrong results.
  3. Create a seven-day rolling mean using only current and past observations.