---
name: time-series-exploration
description: >-
  Runs a careful first look at data indexed by time before any forecast: it
  parses the timestamp correctly, confirms the real frequency and finds gaps,
  duplicates and irregular spacing, plots the raw series, resamples at an honest
  frequency with the aggregate that fits a flow or a stock, uses rolling windows
  to read the trend, separates trend from seasonality from noise with an additive
  or multiplicative decomposition, and watches timezone and daylight-saving
  traps. Use when someone hands you a dated series and asks to "look at this over
  time", "plot the trend", "is there seasonality", "resample to weekly/monthly",
  "why did this spike", "smooth this", "what's the pattern in these daily/hourly
  numbers", or gives you a column of timestamps with a value. Do NOT use for:
  building or evaluating a forecast model (ARIMA, Prophet, ML), anomaly-detection
  models, a first-pass profile of a non-time dataset (use
  exploratory-data-analysis), the shape of one variable (use data-distributions),
  comparing a metric across non-time groups (use grouping-aggregation), or A/B
  test read-outs.
---

# Time-series exploration

Time-indexed data breaks the usual assumptions. The rows are not independent, the order matters, and three different things, the level the series sits at, the direction it is trending, and the pattern that repeats every week or year, are tangled together in one line. The common mistakes all come from skipping the look: resampling a series that turns out to have month-long gaps, averaging a flow that should have been summed, reading a seasonal peak as a change in trend, fitting an additive model to seasonality that clearly grows with the level, and trusting local timestamps across a daylight-saving change that duplicated or dropped an hour. This skill does the first look properly, in a fixed order, so that whatever comes next, a forecast, a dashboard, an alert, is built on a series you have actually understood.

The discipline is to establish the real frequency before resampling, to pick the aggregate from what the measure is, and to separate trend, seasonality and noise before reading any one of them.

## When to use this, and when not to

Use this skill when:

- Someone gives you a series indexed by time (a timestamp or date column plus a value) and wants to know what it looks like over time.
- A question involves trend, seasonality, a spike or dip, smoothing, or resampling to a coarser frequency ("make this weekly", "show me the monthly total").
- A forecast, alert or time-based dashboard is about to be built on a series nobody has looked at yet.
- A number "changed" and someone needs to see whether it is trend, season or a one-off before reacting.

Do not use this skill for:

- Building, tuning or judging a forecast model (ARIMA, exponential smoothing, Prophet, a machine-learning model). This skill ends where forecasting begins; it hands over a clean, understood series.
- Anomaly-detection modelling. It can point at a spike; it does not fit a detector.
- A first-pass profile of a dataset that is not primarily about time. Use `exploratory-data-analysis`.
- The shape of one variable ignoring time (`data-distributions`) or a metric compared across non-time groups (`grouping-aggregation`).
- Reading out an experiment. Use the experiment method for that.

## Inputs you must have before starting

Ask for anything missing. Do not assume it.

1. **The timestamp column and what one row represents.** One row per event, per transaction, or already one row per day, are three different starting points and change whether you aggregate first.
2. **The measure, and whether it is a flow or a stock.** A flow accumulates over an interval (sales, clicks, signups) and is summed when you resample. A stock is a level at a point in time (headcount, inventory, price) and is taken as the last or the mean. Getting this wrong corrupts every resampled figure.
3. **The timezone of the timestamps,** and whether they are local or UTC. Local timestamps across a daylight-saving boundary have a duplicated and a missing hour.
4. **The expected frequency,** if any (hourly readings, daily totals), so you can tell a genuine gap from the normal spacing.
5. **Why you need the look:** to spot a trend, confirm seasonality, explain a spike, choose a resample frequency, or prepare the series for forecasting. The reason decides where to spend effort.
6. **Sensitivity.** If the series is personal, health, financial, legal or employment data, follow the PII rules below; a timestamped series tied to a person can be re-identifying.

If the person gives you a timestamp and a value and nothing else, confirm the flow-versus-stock question (input 2) and the timezone (input 3) before resampling, because both silently change the numbers.

## Workflow

Work on a copy of the data. This skill does not alter the source.

### Step 1: Parse the time correctly and sort

1. Parse the timestamp explicitly rather than letting the loader guess. Pass the format when you know it, and read as UTC when the data is UTC, so a mixed or ambiguous column does not parse half one way and half another.
2. Handle unparseable values on purpose: coerce them to "not a time" and count them, rather than letting one bad string stop the load or shift a column.
3. Set the timestamp as the index and sort by it. Many time-series operations give wrong or surprising results on an unsorted index.
4. Note the timezone. If the timestamps are naive local times and cross a daylight-saving change, flag it now; you will deal with the duplicated and missing hour in step 8.

### Step 2: Establish the real frequency, and find gaps, duplicates and irregular spacing

Never assume the series is evenly spaced. Measure it:

- Compute the gaps between consecutive timestamps. A single dominant gap (every row 1 hour apart) means regular spacing; a spread of gaps means irregular data that must be resampled before most operations make sense.
- Find missing periods: expected timestamps with no row. A week of zero sales and a week of missing data look identical in a total and mean opposite things; a gap is not a zero.
- Find duplicate timestamps: two rows for the same instant. Decide whether to sum them (two events) or treat one as an error, before aggregating.

Decision rule: if the spacing is irregular or has gaps, resample to a fixed frequency (step 4) before computing any rolling statistic or decomposition, and decide explicitly whether a missing period is a zero or a true gap. Do not fill gaps silently.

### Step 3: Plot the raw series first

Draw the line before computing anything. One plot shows the level, the overall direction, any repeating pattern, outliers, a sudden step (a structural break), and the gaps from step 2. For a long or noisy series, also plot a smoothed version (step 5) over the raw one. Read the plot before you trust a number; a decomposition of a series with an obvious break in it will smear the break across the trend and the residual.

### Step 4: Resample to an honest frequency with the aggregate that fits

Resampling to a coarser frequency (daily to weekly, hourly to daily) makes trend and seasonality legible. Pick the aggregate from input 2:

| The measure is | Resample with | Because |
|---|---|---|
| A flow (sales, clicks, signups) | sum | The weekly total is the sum of the daily totals |
| A stock (inventory, price, headcount) | last, or mean | A total of a level is meaningless; you want the level |
| A rate (conversion, defect rate) | sum(numerator) / sum(denominator) over the bin | Averaging the daily rates is the averaging-averages trap (see grouping-aggregation) |

Two cautions. First, know how the bins are labelled: by convention most fixed frequencies are left-closed and left-labelled, while some period-end frequencies (month-end, quarter-end, year-end, weekly) are right-labelled, so a bin can be stamped with its start or its end; confirm the `closed` and `label` behaviour in the resample reference before relying on it (verify before relying on this; pandas resample API). Second, do not upsample to a finer frequency and interpolate without a reason: inventing values between real observations creates data that was never measured.

### Step 5: Smooth with a rolling window to read the trend

A rolling mean or median over a sensible window reveals the trend under the noise. State the window (a 7-day window removes weekly wiggle; a 28-day window removes it more firmly) and whether it is centred or trailing: a centred window aligns the smoothed value with the middle of the window but needs future points, so it cannot reach the latest date; a trailing window reaches the end but lags. A rolling window also produces missing values at the edges where the window is not yet full; say so rather than letting the trend line start late without explanation.

### Step 6: Separate trend, seasonality and noise, and choose the model by eye

To read seasonality and trend as separate things, decompose the series into trend, a repeating seasonal component and a residual. Two decisions:

- **Additive or multiplicative.** If the size of the seasonal swing stays roughly constant as the level rises, use an additive model (series = trend + seasonal + residual). If the swing grows with the level (December is always 30% above the year, not a fixed amount above it), use a multiplicative model (series = trend x seasonal x residual), or take the logarithm and decompose additively (statsmodels seasonal_decompose, verified 2026-10-27).
- **The period.** You must tell the decomposition the length of the cycle (7 for weekly structure in daily data, 12 for monthly-in-yearly), and you need at least two full cycles of data for it to work at all (statsmodels seasonal_decompose, verified 2026-10-27).

Treat the classical moving-average decomposition as a first read: the documentation itself calls it naive and points to more robust methods such as STL (season-trend decomposition using LOESS) for anything load-bearing (statsmodels seasonal_decompose, verified 2026-10-27). Then look at the residual: if it still has a visible pattern, the model or the period is wrong, because a good decomposition leaves residual that looks like noise.

### Step 7: Read trend, seasonality and breaks as separate findings

Report them one at a time: the overall trend (rising, flat, falling, and roughly how fast), the seasonality (its period and rough size), any structural break or regime change (a step after a launch, an outage, a pricing change), and any outliers. A seasonal peak is not a trend change, and a one-off spike is not seasonality; keeping them separate is the whole point of the look. A rough stationarity read (does the rolling mean or variance drift over time?) is useful only to tell the next step what it is dealing with; this skill does not test for it formally or difference the series.

### Step 8: Handle timezone and calendar effects honestly

- If the timestamps are naive local times crossing a daylight-saving change, one local hour is duplicated and one is missing. Localise to the real timezone so the duplicated hour is disambiguated and the missing hour is handled, or work in UTC and convert only for display (pandas time series user guide, verified 2026-10-27).
- Remember that calendar arithmetic and elapsed-time arithmetic differ across a DST boundary: adding "one day" as a calendar offset keeps the wall-clock time, while adding 24 hours as elapsed time does not (pandas time series user guide, verified 2026-10-27).
- Watch month length (February totals look low per month but not per day), business days versus calendar days, and holidays, before reading a dip as a real change.

### Step 9: Write it up, then verify

Report the span and frequency, the gaps and duplicates found, the trend, the seasonality with its period, any breaks or outliers, and what it means for the next step. Then run the quality bar below. Recompute one resampled figure a second way from the raw rows (a weekly total by summing the raw daily rows in that week); if it does not match, the resampling or the aggregate is wrong.

## Worked example

**Input:** "Here are our daily signups for the last 18 months, one row per day, `date` and `signups`. What's going on over time?"

**Step 1 to 2.** Dates parse cleanly and sort. The spacing is one day, but 11 dates are missing in a single stretch in March: a known outage, a true gap, not zeros. No duplicate dates.

**Step 3, plot.** The raw line rises over the 18 months, wiggles within each week, has one sharp spike in July (a campaign), and the March gap.

**Step 4, resample.** Signups are a flow, so weekly figures are sums. The March gap is left as missing, not summed to zero, so the trend is not dragged down by a fake low week.

**Step 5, smooth.** A 28-day centred rolling mean on the daily series shows a steady upward trend with the July spike standing clearly above it.

**Step 6, decompose.** Weekly structure in daily data means period 7. The weekly swing is a roughly constant number of signups regardless of the level, so an additive model fits; the residual is flat apart from July, which confirms the spike is a one-off, not seasonality. Weekends are consistently low.

**Step 7 to 8.** Trend: rising, roughly steady. Seasonality: weekly (period 7), weekends down. Break/outlier: the July campaign spike, and the March outage gap. Timestamps are dates with no time, so no DST issue, but the weekend effect means a Monday public holiday will read low.

**Write-up (shortened):**

> **Daily signups, 18 months (one row per day), 2026-10-27.** Clear upward trend; weekly seasonality (period 7) with weekends low; one campaign spike in July; an 11-day data gap in March (outage), left as missing, not zero. Weekly resample uses sums (signups are a flow). An additive decomposition fits (constant weekly swing) and leaves clean residual apart from July. **For forecasting:** use weekly seasonality of 7, treat the March gap and July spike explicitly, and expect Monday-holiday weeks to read low. Hand the modelling to a forecasting method.

## Templates

### Time-series look write-up (use verbatim)

```
# <measure> over time, <grain>, <date>
Span: <start> to <end>   Frequency: <real spacing>   Measure type: <flow / stock / rate>
Gaps/duplicates: <missing periods (true gap vs zero), duplicate timestamps>
Resample: <target freq> with <sum / last / mean / ratio of sums>, bins <closed/label>
Trend: <rising / flat / falling, rough rate>
Seasonality: <period, rough size, additive or multiplicative>
Breaks / outliers: <steps, spikes, their dates and likely cause>
Timezone: <UTC / local; DST handled how>
What it means: <the honest read, and the next step; forecasting handed off>
```

### Opening questions when inputs are thin

```
Three things so the series reads honestly:
1. What does one row represent, and is the value a flow (sum it) or a stock (take the level)?
2. What timezone are the timestamps in, and are they local or UTC?
3. What's the expected spacing, so I can tell a real gap from normal spacing?
```

## Failure modes and anti-patterns

1. **Assuming even spacing.** Running a rolling window or a decomposition on a series with gaps, duplicates or irregular spacing, so the window covers the wrong span and the seasonality is misaligned. Spot it by measuring the gaps between timestamps before anything else. Fix it by resampling to a fixed frequency and deciding gap-versus-zero explicitly.
2. **The wrong resample aggregate.** Averaging a flow (so a quiet half of a month halves the monthly "total") or summing a stock (so inventory "accumulates"). Spot it by asking whether the measure is a flow or a stock. Fix it with sum for flows, last or mean for stocks, and a ratio of sums for rates.
3. **Confusing trend with seasonality.** Reading a seasonal peak as a change in direction, or a rising trend as a run of good months. Spot it when a claim about trend has not separated out the repeating pattern. Fix it by decomposing first and reading trend, seasonality and residual as separate components.
4. **The wrong decomposition model.** Fitting an additive model when the seasonal swing clearly grows with the level, so the residual keeps a seasonal shape. Spot it when the residual still has a pattern, or when the seasonal band fans out on the raw plot. Fix it with a multiplicative model or a log transform, and prefer STL over the naive method for anything that matters.
5. **Timezone and DST mishandling.** Treating naive local timestamps as if time were continuous across a daylight-saving change, so an hour is silently duplicated or missing, or comparing times from two zones as if they were the same. Spot it by checking the timezone and whether the span crosses a DST boundary. Fix it by localising to the real timezone or working in UTC and converting only for display.

## Quality bar

Run this against your own output. Every item must be verifiable.

- [ ] The timestamp was parsed explicitly, the index sorted, and unparseable values counted rather than dropped silently.
- [ ] The real frequency was measured, and gaps, duplicates and irregular spacing were found and reported.
- [ ] Missing periods were treated as gaps or as zeros on purpose, not filled silently.
- [ ] The raw series was plotted before any statistic was trusted.
- [ ] The resample aggregate matches the measure type (sum for flows, last or mean for stocks, ratio of sums for rates).
- [ ] The rolling window size and whether it is centred or trailing were stated, and edge gaps acknowledged.
- [ ] A decomposition separated trend, seasonality and residual, with the model (additive or multiplicative) and period justified, and the residual checked for leftover pattern.
- [ ] Trend, seasonality, breaks and outliers were reported as separate findings.
- [ ] Timezone was stated and any DST boundary handled.
- [ ] One resampled figure was recomputed from the raw rows and matches.
- [ ] If the series is sensitive, no re-identifying raw timeline was printed or sent anywhere.

## Guardrails

- **Do not fabricate.** Every figure comes from the data. The flow-versus-stock rule, the decomposition models, the two-cycle minimum and the DST cautions are method, labelled as such. Anything version-dependent (the default bin labelling of a resample frequency) carries a "verify before relying on this" marker, because it has changed between tool versions.
- **Work on a copy; confirm before changing data.** Filling a gap, interpolating, dropping an outlier or smoothing in place are changes the person approves first, and the reader is told the series was altered. Report the series as observed before reshaping it.
- **PII and credentials.** A timestamped series tied to a person (logins, locations, transactions) can re-identify them even when aggregated finely. Never print a raw personal timeline, aggregate to a coarse enough frequency that no individual's pattern is exposed, and do not paste personal time-series into a third-party service.
- **Not advice.** When the series concerns health, money, legal standing or employment, the output describes the series over time only. It is not medical, financial, legal or HR advice, and a trend is not a forecast; a decision needs a qualified human.
- **Cost.** Plotting, resampling and rolling windows are cheap and local. A decomposition or an STL fit on a long, high-frequency series can be slow and memory-hungry; resample to a sensible frequency first and say the analysis is on the resampled series.
- **Scope and no duplication.** This skill looks at a series over time and hands a clean, understood series to whatever comes next. It does not forecast, fit an anomaly detector or test for stationarity formally. For a non-time dataset profile use `exploratory-data-analysis`; for one variable's shape use `data-distributions`; for a metric across non-time groups use `grouping-aggregation`.

## Tool appendix (Python / pandas, statsmodels)

```
This section is tool-specific. The workflow above is tool-agnostic and works
the same in R (xts, forecast, stl) or a time-series database.
```

- Parse and index: `df["ts"] = pd.to_datetime(df["ts"], utc=True, format="...", errors="coerce")`; `df = df.set_index("ts").sort_index()`. `errors="coerce"` turns bad values into `NaT` so you can count them; the default `"raise"` stops the load (pandas time series user guide, verified 2026-10-27).
- Measure spacing and gaps: `df.index.to_series().diff().value_counts()` shows the real frequency; `pd.date_range(start, end, freq=...).difference(df.index)` lists missing periods; `df.index.duplicated().sum()` finds duplicate timestamps.
- Resample a flow: `df["signups"].resample("W").sum()`; a stock: `.resample("D").last()` or `.mean()`; a rate: resample numerator and denominator separately and divide. Confirm `closed`/`label` defaults in the `resample` API reference before relying on bin edges (verify before relying on this).
- `asfreq` versus `resample`: `asfreq` reindexes to a frequency and picks the value at each stamp without aggregating; `resample` aggregates within each bin. Use `resample` when changing granularity.
- Smooth: `df["signups"].rolling(window=28, center=True).mean()`; `center=True` aligns to the middle but cannot reach the latest dates; edges are `NaN` until the window fills.
- Decompose: `from statsmodels.tsa.seasonal import seasonal_decompose; seasonal_decompose(series, model="additive", period=7)`; use `model="multiplicative"` (or log then additive) when the seasonal swing grows with the level; needs at least two full cycles. Prefer `from statsmodels.tsa.seasonal import STL; STL(series, period=7).fit()` for a more robust decomposition (statsmodels, verified 2026-10-27).
- Timezone: `df.index.tz_localize("Europe/London")` attaches a zone and resolves DST; `df.index.tz_convert("UTC")` changes zone keeping the instant. Localise naive local times before comparing across a DST boundary (pandas time series user guide, verified 2026-10-27).

## Sources

- pandas, "Time series / date functionality" user guide (pandas 3.1.0; `to_datetime` parsing and `errors`, timezone localisation and conversion, DST and calendar-versus-elapsed arithmetic, resample example), pandas.pydata.org (last verified 2026-10-27).
- pandas, `DataFrame.resample` API reference (pandas 3.1.0; `closed`/`label` conventions by frequency), pandas.pydata.org (last verified 2026-10-27).
- statsmodels, `tsa.seasonal.seasonal_decompose` API reference (v0.14.4; additive and multiplicative models, period requirement, two-cycle minimum, naive method, STL pointer), statsmodels.org (last verified 2026-10-27).
- R. J. Hyndman and G. Athanasopoulos, "Forecasting: Principles and Practice" (3rd ed.), ch. on time-series decomposition (trend, seasonal and remainder; STL), otexts.com/fpp3 (last verified 2026-10-27).
- Anthropic, Agent Skills repository and skill-authoring guidance, github.com/anthropics/skills (reviewed as the reference for skill triggering and structure; no time-series skill exists there) (last verified 2026-10-27).
- Representative community "data-analysis" SKILL.md files (for example Mindrally/skills), reviewed as existing implementations: generic pandas best-practice lists with no frequency check, resample-aggregate rule or DST handling (last verified 2026-10-27).

Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo, link to afeezadebayo.com and to this skill's page. LinkedIn: linkedin.com/in/afeez-adebayo.
