Tutorial

Time Series for Analysts: Trend, Seasonality, and the 7-Line Forecast

Decompose trend and seasonality, resample daily to monthly, smooth with rolling windows, and build a naive forecast in Python.

Anuj SainiAug 23, 20265 min read

Dashboards show what happened. Time series tells you whether it will repeat. This playbook builds a daily sales series with trend plus weekly seasonality, then resamples, smooths, and forecasts it using the generator in courses/workbooks/generators/time_series.py.

What does the notebook build end-to-end?

A complete analyst loop: generate -> index as DatetimeIndex -> resample to weekly/monthly -> smooth with rolling -> decompose into trend and seasonality -> naive forecast with error bands. The same cells run on your own exports after replacing the synthesis block. Anchor the mental model with Pandas fundamentals for indexing and the EDA playbook for distribution checks before any forecast.

Ingredients: 731 daily rows (2023-01-01 to 2024-12-31), linear trend 100->300, sine seasonality (period 7), and Gaussian noise. Seed 42.

How do you create and index a realistic time series?

Setup and synthesis:

python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style='darkgrid')
python
dates = pd.date_range(start='2023-01-01', end='2024-12-31', freq='D')
n = len(dates)
trend = np.linspace(start=100, stop=300, num=n)
seasonality = 20 * np.sin(2 * np.pi * np.arange(n) / 7)  # weekly
noise = np.random.normal(0, 10, n)
sales = trend + seasonality + noise
df = pd.DataFrame({'date': dates, 'sales': sales}).set_index('date')
print(df.head())
print(df.tail())

Rendered output: a 5-row head showing 2023-01-01 sales ~104, a visible 7-day ripple in the first fortnight, and the date index dtype datetime64[ns] confirming correct indexing for every downstream method.

How do you resample and smooth?

Two verbs, two purposes.

python
# Resample: daily -> weekly and monthly totals
weekly = df['sales'].resample('W').mean()
monthly = df['sales'].resample('ME').mean()
print(weekly.head(3))
print(monthly.head(3))
 
# Smooth: 7-day rolling mean (centred for decomposition)
df['roll_7'] = df['sales'].rolling(window=7, center=True).mean()
df[['sales','roll_7']].plot(figsize=(12,4), title='Daily Sales vs 7-Day Mean')
plt.show()

Rendered output: weekly mean smooths the sine from +/-20 to +/-5; the plot shows the raw series as thin jag and the rolling mean as a clean trend line lagging by 3 days. Centred rolling introduces 3 NaNs at each edge — expected.

How do you separate trend from seasonality?

Manual decomposition before reaching for libraries:

python
# Trend as 30-day rolling, seasonality as residual pattern, residual as remainder
df['trend_30'] = df['sales'].rolling(30, center=True).mean()
df['detrended'] = df['sales'] - df['trend_30']
 
# Average detrended by day-of-week to reveal seasonality
df['dow'] = df.index.day_name()
seasonal_profile = df.groupby('dow')['detrended'].mean()
print(seasonal_profile.sort_values())

Rendered output: Monday troughs ~-12 and Saturday peaks ~+14, matching the injected sine. Residual sales - trend - seasonality centres near zero with std ~10, matching the noise term.

How do you forecast and when is naive enough?

Start with the baseline every fancier model must beat.

python
# Naive: next 7 days = last 7 days (seasonal naive)
last_week = df['sales'].iloc[-7:].values
forecast_index = pd.date_range(start=df.index.max() + pd.Timedelta(days=1), periods=7, freq='D')
naive_forecast = pd.Series(last_week, index=forecast_index, name='forecast')
 
# Plot history tail + forecast
df['sales'].iloc[-60:].plot(figsize=(12,4), label='history')
naive_forecast.plot(label='naive forecast', style='--')
plt.legend()
plt.show()
 
# Simple error: MAE on hold-out last 30 days
holdout = df['sales'].iloc[-30:]
pred = df['sales'].shift(7).iloc[-30:]  # prior week as prediction
mae = (holdout - pred).abs().mean()
print(f'Naive MAE on 30-day holdout: {mae:.1f}')
Feature / Criteria

Gotcha: Missing Dates Break Rolling and Resample

Daily data with skipped holidays (missing rows) makes a 7-row rolling window span 9 calendar days, not 7, silently shifting the trend. Fix with df = df.asfreq('D') to insert NaNs for missing dates, then interpolate or ffill explicitly before any window. The notebook shows MAE jumping 10->18 when a 4-day gap is left unfilled.

What do you practise next?

When a stakeholder asks "why did sales spike?", answer with decomposition, not a single number. Then validate against SQL date functions if the source is a warehouse export, and publish the monthly rollup as a stakeholder chart using Excel charts.


Download the Notebook and Practise

This article is a walkthrough of a runnable Jupyter notebook. Download the original .ipynb and run it locally or on Colab — every code block above appears in order.

Download the Time Series Playbook Notebook

Get the complete .ipynb with outputs — runs on any Python 3.10+ environment with pandas, numpy, and the libraries listed in setup.

Download .ipynb

Continue your track: Data Analyst Roadmap · Python and Pandas Guide · SQL NULL Handbook · SQL JOIN Fan-Out · Topfolio Practice · Data Analyst vs Engineer

Dataset generators where applicable are in courses/workbooks/generators/ — see citations atop for the exact *.py source for this notebook.


Frequently Asked Questions

What is the difference between resample and rolling in Pandas?

resample changes frequency (daily -> monthly) while rolling slides a window over the original frequency to smooth (e.g., 7-day mean). Use both: resample for reports, rolling for noise reduction.

How do I split trend from seasonality manually?

Compute a rolling mean as trend, subtract it from the series to reveal seasonality, and inspect the residual. For production, prefer statsmodels.tsa.seasonal.seasonal_decompose.

Should I fill missing dates in a time series?

Yes — reindex to a complete DatetimeIndex and decide: forward-fill for cumulative metrics, interpolate for rates, zero for count data. Gaps otherwise distort rolling and resample.

What is a naive forecast?

The simplest baseline: next period equals last period (or last seasonal period). Always ship it first; a fancier model must beat it to justify complexity.

Frequently Asked Questions

What is the difference between resample and rolling in Pandas?

resample changes frequency (daily -> monthly) while rolling slides a window over the original frequency to smooth (e.g., 7-day mean). Use both: resample for reports, rolling for noise reduction.

How do I split trend from seasonality manually?

Compute a rolling mean as trend, subtract it from the series to reveal seasonality, and inspect the residual. For production, prefer statsmodels.tsa.seasonal.seasonal_decompose.

Should I fill missing dates in a time series?

Yes — reindex to a complete DatetimeIndex and decide: forward-fill for cumulative metrics, interpolate for rates, zero for count data. Gaps otherwise distort rolling and resample.

What is a naive forecast?

The simplest baseline: next period equals last period (or last seasonal period). Always ship it first; a fancier model must beat it to justify complexity.

Anuj Saini

Written by

Anuj SainiFounder & Lead Instructor

Founder at Topfolio with 6+ years in data & analytics across JPMC, Ultrahuman, and high-growth startups. Sat on hiring panels, reviewed 500+ resumes, and writes practical SQL & data guides.