Time Series Analysis

A time series is a sequence of data points indexed in time order — stock prices, weather readings, sensor data, sales figures. Time series analysis extracts patterns to understand the past and forecast the future.

import pandas as pd
import matplotlib.pyplot as plt

# Load a simple time series
df = pd.read_csv('data.csv', parse_dates=['date'], index_col='date')
df['value'].plot(figsize=(12, 4), title='Time Series')
plt.show()

Components of a Time Series

Every time series can be decomposed into four components:

T

Trend

Long-term increase or decrease. E.g., a company's revenue growing year over year.

S

Seasonality

Repeating pattern within a fixed period. E.g., ice cream sales peak every summer.

C

Cyclicality

Long irregular fluctuations not tied to a calendar period. E.g., economic boom-bust cycles.

R

Residual (Noise)

Random variation left after removing trend, seasonality, and cycles.

from statsmodels.tsa.seasonal import seasonal_decompose

result = seasonal_decompose(df['value'], model='additive', period=12)
result.plot()
plt.tight_layout()
plt.show()
# Plots: original, trend, seasonal, residual

Stationarity

A time series is stationary if its statistical properties (mean, variance, autocorrelation) do not change over time. Most classical forecasting models require stationary data.

The Augmented Dickey-Fuller (ADF) test checks for stationarity — a p-value below 0.05 means the series is stationary.

from statsmodels.tsa.stattools import adfuller

def check_stationarity(series):
    result = adfuller(series.dropna())
    print(f'ADF Statistic : {result[0]:.4f}')
    print(f'p-value       : {result[1]:.4f}')
    print('Stationary ✓' if result[1] < 0.05 else 'Not stationary — try differencing')

check_stationarity(df['value'])

Making a Series Stationary

If the series is not stationary, differencing (subtracting the previous value) often helps:

# First-order differencing
df['diff1'] = df['value'].diff()
check_stationarity(df['diff1'])

ARIMA

ARIMA(p, d, q) combines three components:

  • AR(p) — AutoRegressive: the value depends on its own past p values
  • I(d) — Integrated: d rounds of differencing to make the series stationary
  • MA(q) — Moving Average: the value depends on the past q forecast errors
from statsmodels.tsa.arima.model import ARIMA
import warnings
warnings.filterwarnings('ignore')

# Fit ARIMA(1,1,1) — a common starting point
model = ARIMA(df['value'], order=(1, 1, 1))
result = model.fit()
print(result.summary())

# Forecast next 12 steps
forecast = result.forecast(steps=12)
print(forecast)

Choosing p, d, q with ACF/PACF

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
plot_acf(df['value'].diff().dropna(), ax=axes[0])   # guides q
plot_pacf(df['value'].diff().dropna(), ax=axes[1])  # guides p
plt.show()

Moving Average Explorer

Adjust the window size to see how SMA (Simple Moving Average) and EMA (Exponential Moving Average) smooth a noisy signal. Switch signal presets to explore different patterns.

5

Raw Signal

After SMA

Noise Reduction

LSTM Forecasting

Long Short-Term Memory (LSTM) networks are a type of RNN that can learn long-range dependencies in sequential data — making them powerful for time series forecasting when patterns are complex or non-linear.

Prepare Sequences

import numpy as np
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
scaled = scaler.fit_transform(df[['value']])

def create_sequences(data, window=30):
    X, y = [], []
    for i in range(len(data) - window):
        X.append(data[i:i+window])
        y.append(data[i+window])
    return np.array(X), np.array(y)

X, y = create_sequences(scaled)
split = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]

Build and Train the LSTM

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout

model = Sequential([
    LSTM(64, return_sequences=True, input_shape=(30, 1)),
    Dropout(0.2),
    LSTM(32),
    Dense(1)
])
model.compile(optimizer='adam', loss='mse')
model.fit(X_train, y_train, epochs=20, batch_size=32,
          validation_split=0.1, verbose=1)

# Predict and inverse-transform
preds = scaler.inverse_transform(model.predict(X_test))

Knowledge Check

Test your understanding — pick the best answer, then click Check.

1. What does stationarity mean in a time series?

2. What does the I in ARIMA stand for?

3. Why is differencing applied before fitting ARIMA?