Data Science

Experimentation And Applied Statistics

Time Series Analysis Basics

Every dataset examined so far has treated rows as independent, interchangeable observations — shuffling them wouldn't change any statistic. Time series data bre

JrCodex·7 min read

Jr Codex Data Science Notes

Level: Intermediate–Advanced Prerequisites: Chapter 3: Common Experiment Design Pitfalls Time to complete: ~25 minutes


Table of Contents

  1. What Makes Time Series Data Different
  2. The Components of a Time Series
  3. Visualizing a Time Series
  4. Moving Averages — Smoothing Out Noise
  5. Decomposition
  6. A Simple Forecast — Naive & Moving Average Methods
  7. Why Standard Correlation Can Mislead on Time Series
  8. Summary & Next Steps

1. What Makes Time Series Data Different

Every dataset examined so far has treated rows as independent, interchangeable observations — shuffling them wouldn't change any statistic. Time series data breaks that assumption: the order matters, and today's value is often directly influenced by yesterday's.

Regular Tabular Data                  Time Series Data
─────────────────────────────         ─────────────────────────────
  Each row = one customer                Each row = one TIME POINT
  Shuffling rows changes NOTHING           Shuffling rows DESTROYS the
  about the analysis                        entire meaning of the data
─────────────────────────────         ─────────────────────────────

2. The Components of a Time Series

A Time Series Is Typically Decomposed Into
─────────────────────────────────────────
  Trend:        the long-term overall direction (growing, shrinking, flat)
  Seasonality:   a REPEATING pattern at a fixed, known frequency
                  (daily, weekly, yearly — e.g. retail spikes every December)
  Noise/Residual: whatever's left after removing trend and seasonality —
                    the unpredictable, random component
─────────────────────────────────────────
import numpy as np
import pandas as pd
 
np.random.seed(0)
dates = pd.date_range("2022-01-01", periods=365 * 2, freq="D")
 
trend = np.linspace(100, 200, len(dates))                              # steadily increasing
seasonality = 20 * np.sin(2 * np.pi * dates.dayofyear / 365)              # yearly cycle
noise = np.random.normal(0, 5, len(dates))                                 # random day-to-day variation
 
values = trend + seasonality + noise
df = pd.DataFrame({"date": dates, "value": values}).set_index("date")

3. Visualizing a Time Series

import matplotlib.pyplot as plt
 
fig, ax = plt.subplots(figsize=(12, 4))
ax.plot(df.index, df["value"])
ax.set_title("Raw Time Series")
ax.set_xlabel("Date")
ax.set_ylabel("Value")

Directly applying Module 4's chart-selection principle — a line chart is the correct default for time series, since it visually preserves the sense of continuity and order that a bar chart would obscure.


4. Moving Averages — Smoothing Out Noise

A moving average (or "rolling average") replaces each point with the average of itself and its nearby neighbors — smoothing out short-term noise to reveal the underlying trend more clearly.

import pandas as pd
 
df["ma_7"] = df["value"].rolling(window=7).mean()          # 7-day moving average
df["ma_30"] = df["value"].rolling(window=30).mean()          # 30-day moving average
 
import matplotlib.pyplot as plt
 
fig, ax = plt.subplots(figsize=(12, 4))
ax.plot(df.index, df["value"], alpha=0.3, label="Raw data")
ax.plot(df.index, df["ma_7"], label="7-day moving average")
ax.plot(df.index, df["ma_30"], label="30-day moving average", linewidth=2)
ax.legend()
ax.set_title("Raw Data vs Moving Averages")
Choosing a Window Size
─────────────────────────────────────────
  Shorter window (e.g. 7-day)   → tracks the data more closely,
                                    still shows some noise
  Longer window (e.g. 30-day)     → smoother, reveals the broader
                                      trend, but reacts SLOWER to
                                      genuine recent changes
─────────────────────────────────────────

Practical use: moving averages are the standard way to answer "is this metric actually trending up, or is that just noisy day-to-day variation?" — a version of this question comes up constantly when monitoring business metrics or, in Chapter 3's A/B testing context, tracking a metric across a test's duration.


5. Decomposition

Rather than eyeballing trend/seasonality/noise separately, statsmodels can split them apart programmatically:

from statsmodels.tsa.seasonal import seasonal_decompose
import matplotlib.pyplot as plt
 
result = seasonal_decompose(df["value"], model="additive", period=365)
 
fig, axes = plt.subplots(4, 1, figsize=(12, 10))
result.observed.plot(ax=axes[0], title="Observed")
result.trend.plot(ax=axes[1], title="Trend")
result.seasonal.plot(ax=axes[2], title="Seasonality")
result.resid.plot(ax=axes[3], title="Residual (Noise)")
plt.tight_layout()
Additive vs Multiplicative Decomposition
─────────────────────────────────────────
  Additive:        value = trend + seasonality + noise
                     use when seasonal swings stay roughly the SAME
                     SIZE regardless of the trend's level

  Multiplicative:    value = trend × seasonality × noise
                       use when seasonal swings GROW proportionally
                       as the trend grows (e.g. holiday sales spikes
                       that get bigger as the business itself grows)
─────────────────────────────────────────

6. A Simple Forecast — Naive & Moving Average Methods

Full forecasting (ARIMA, Prophet, and beyond) is a deep topic of its own — this section covers the simplest baseline methods, useful both on their own for short horizons and as a sanity-check baseline against any fancier model.

import pandas as pd
 
# Naive forecast: "tomorrow will look like today" — surprisingly hard to beat for short horizons
last_value = df["value"].iloc[-1]
naive_forecast = last_value      # forecast for the next period
 
# Moving average forecast: "tomorrow will look like the recent average"
ma_forecast = df["value"].tail(30).mean()
 
print(f"Naive forecast: {naive_forecast:.1f}")
print(f"Moving average forecast: {ma_forecast:.1f}")
# Evaluating a forecast method — held-out validation, the time-series version of a train/test split
train = df.iloc[:-30]      # everything except the last 30 days
test = df.iloc[-30:]         # the last 30 days, held out to check forecast accuracy
 
naive_predictions = [train["value"].iloc[-1]] * len(test)      # repeat the last known value
mae = (test["value"] - naive_predictions).abs().mean()
print(f"Naive forecast MAE: {mae:.2f}")      # Mean Absolute Error — average forecast miss
Why the Split Isn't Random (Unlike Module 1's Sampling)
─────────────────────────────────────────
  A time series train/test split must be CHRONOLOGICAL —
  train on the PAST, test on the FUTURE.

  Randomly shuffling rows before splitting (as you might for
  ordinary tabular data) would let the model "see the future"
  during training — an invalid, overly optimistic evaluation.
  This exact distinction reappears in the Machine Learning
  notes' model evaluation chapter.
─────────────────────────────────────────

7. Why Standard Correlation Can Mislead on Time Series

Module 1, Chapter 5's correlation warning gets an extra twist with time series: two variables can appear strongly correlated purely because they both trend over time, with no real relationship between them at all — a phenomenon called spurious correlation.

import numpy as np
import pandas as pd
 
np.random.seed(1)
 
# Two COMPLETELY UNRELATED series, that both happen to trend upward over time
dates = pd.date_range("2020-01-01", periods=100, freq="M")
series_a = np.linspace(10, 50, 100) + np.random.normal(0, 2, 100)      # e.g. "ice cream sales"
series_b = np.linspace(5, 200, 100) + np.random.normal(0, 10, 100)       # e.g. "global CO2 levels"
 
correlation = np.corrcoef(series_a, series_b)[0, 1]
print(f"Correlation: {correlation:.3f}")      # very high, e.g. 0.95+ — despite NO real relationship!
The Fix — Compare CHANGES, Not Levels
─────────────────────────────────────────
  Instead of correlating the raw trending series, correlate their
  period-over-period CHANGES (differences) — this removes the
  shared trend and reveals whether they actually move together.
─────────────────────────────────────────
import pandas as pd
 
diff_a = pd.Series(series_a).diff().dropna()
diff_b = pd.Series(series_b).diff().dropna()
 
correlation_diff = diff_a.corr(diff_b)
print(f"Correlation of differences: {correlation_diff:.3f}")      # much closer to 0 — the honest answer

This is a direct, practical extension of Module 1, Chapter 5's "correlation is not causation" — with time series, even correlation itself can be an illusion created by a shared trend, not just a confound.


8. Summary & Next Steps

Key Takeaways

  • Time series data breaks the "rows are independent and interchangeable" assumption every prior module relied on — order carries meaning, and shuffling destroys it.
  • Every time series can be thought of as trend + seasonality + noise; seasonal_decompose splits these apart explicitly.
  • Moving averages smooth short-term noise to reveal the underlying trend — shorter windows track closely but stay noisy, longer windows smooth more but react slower.
  • Time series train/test splits must be chronological (train on the past, test on the future) — never randomly shuffled, unlike ordinary tabular splits.
  • Two trending series can show high correlation purely from sharing a trend, with no real relationship — correlating their period-over-period changes instead is the standard fix.

Module 5 Complete (through Chapter 4) — Continuing to Chapter 5

You now have the applied statistical toolkit for experimentation and time-ordered data. Chapter 5 closes this module with a different but essential skill: translating everything you've learned across this curriculum into a narrative a non-technical stakeholder will actually act on.

Concept Check

  1. Why can't you randomly shuffle rows before doing a train/test split on time series data, the way you might for ordinary tabular data?
  2. What's the difference between additive and multiplicative decomposition, and when would you choose each?
  3. Why might two completely unrelated metrics show a very high correlation if both happen to be trending upward over time?

Next Chapter

Chapter 5: Storytelling with Data


Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index