Lag Plot for Time Series Autocorrelation Diagnosis — Pygal

A lag plot is a scatter plot of a time series against a lagged version of itself, plotting y(t) on the x-axis versus y(t+k) on the y-axis for a given lag order k. If the data is purely random, points scatter uniformly with no visible structure; if autocorrelation is present, distinctive patterns emerge — linear clusters for autoregressive processes, elliptical shapes for seasonal data. This provides a quick visual diagnostic for time series dependence, complementing numerical tools like ACF/PACF.

Lag Plot for Time Series Autocorrelation Diagnosis rendered with Pygal

Python source (Pygal)

""" anyplot.ai
scatter-lag: Lag Plot for Time Series Autocorrelation Diagnosis
Library: pygal 3.1.3 | Python 3.13.14
Quality: 89/100 | Updated: 2026-06-24
"""

import os
import sys

import numpy as np


# Avoid name collision: pygal.py filename shadows the pygal package
_cwd = sys.path[0] if sys.path[0] else "."
if _cwd in sys.path:
    sys.path.remove(_cwd)

import pygal
from pygal.style import Style


# Theme tokens — Imprint palette, theme-adaptive chrome
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint categorical palette for temporal quartiles + structural reference layers
PALETTE = (
    "#009E73",  # Q1 — brand green (ALWAYS first)
    "#C475FD",  # Q2 — lavender
    "#4467A3",  # Q3 — blue
    "#BD8233",  # Q4 — ochre
    INK_MUTED,  # +1σ envelope (structural, muted)
    INK_MUTED,  # −1σ envelope (structural, muted)
    INK_SOFT,  # y = x reference diagonal
)

# Data — synthetic AR(1) hourly temperature process, moderate positive autocorrelation
np.random.seed(42)
n = 400
phi = 0.78
noise = np.random.normal(0, 1.0, n)
temperature = np.zeros(n)
temperature[0] = 20.0
for i in range(1, n):
    temperature[i] = 20.0 + phi * (temperature[i - 1] - 20.0) + noise[i]

lag = 1
y_t = temperature[:-lag]
y_t_lag = temperature[lag:]

# Temporal quartile masks — color by time to reveal temporal structure
time_idx = np.arange(len(y_t))
q_bounds = np.percentile(time_idx, [25, 50, 75])

early = [
    {"value": (float(y_t[i]), float(y_t_lag[i])), "label": f"Hour {i + 1}"}
    for i in range(len(y_t))
    if time_idx[i] < q_bounds[0]
]
mid_early = [
    {"value": (float(y_t[i]), float(y_t_lag[i])), "label": f"Hour {i + 1}"}
    for i in range(len(y_t))
    if q_bounds[0] <= time_idx[i] < q_bounds[1]
]
mid_late = [
    {"value": (float(y_t[i]), float(y_t_lag[i])), "label": f"Hour {i + 1}"}
    for i in range(len(y_t))
    if q_bounds[1] <= time_idx[i] < q_bounds[2]
]
late = [
    {"value": (float(y_t[i]), float(y_t_lag[i])), "label": f"Hour {i + 1}"}
    for i in range(len(y_t))
    if time_idx[i] >= q_bounds[2]
]

# Correlation coefficient for title annotation
r = np.corrcoef(y_t, y_t_lag)[0, 1]

# Reference geometry — diagonal y = x and ±1σ spread envelope
data_min = float(min(y_t.min(), y_t_lag.min()))
data_max = float(max(y_t.max(), y_t_lag.max()))
pad = (data_max - data_min) * 0.05
ref_start = data_min - pad
ref_end = data_max + pad
ref_line = [(ref_start, ref_start), (ref_end, ref_end)]

sigma = float(np.std(y_t_lag - y_t))
upper_env = [(ref_start, ref_start + sigma), (ref_end, ref_end + sigma)]
lower_env = [(ref_start, ref_start - sigma), (ref_end, ref_end - sigma)]

# Title — length-aware font scaling (baseline 66px for ~67 chars)
title = f"Lag Plot (k={lag}, r={r:.2f}) · scatter-lag · python · pygal · anyplot.ai"
title_len = len(title)
title_font_size = round(66 * 67 / title_len) if title_len > 67 else 66
title_font_size = max(title_font_size, 44)

font = "DejaVu Sans, Helvetica, Arial, sans-serif"
custom_style = Style(
    background=PAGE_BG,
    plot_background=PAGE_BG,
    foreground=INK,
    foreground_strong=INK,
    foreground_subtle=INK_MUTED,
    colors=PALETTE,
    font_family=font,
    title_font_family=font,
    title_font_size=title_font_size,
    label_font_size=56,
    major_label_font_size=44,
    legend_font_size=44,
    legend_font_family=font,
    value_font_size=36,
    tooltip_font_size=32,
    tooltip_font_family=font,
    opacity=0.45,
    opacity_hover=0.95,
    stroke_width=2.5,
)

# Chart — canvas at 3200×1800 (landscape 16:9, hard contract)
chart = pygal.XY(
    width=3200,
    height=1800,
    style=custom_style,
    title=title,
    x_title="y(t)  — Temperature (°C)",
    y_title=f"y(t+{lag})  — Temp. (°C)",
    show_legend=True,
    legend_at_bottom=True,
    legend_at_bottom_columns=4,
    legend_box_size=28,
    stroke=False,
    dots_size=8,
    show_x_guides=True,
    show_y_guides=True,
    x_value_formatter=lambda x: f"{x:.1f}",
    value_formatter=lambda y: f"{y:.1f}",
    margin_bottom=65,
    margin_left=80,
    margin_right=30,
    margin_top=40,
    range=(ref_start, ref_end),
    xrange=(ref_start, ref_end),
    x_labels_major_count=8,
    y_labels_major_count=8,
    print_values=False,
    print_zeroes=False,
    truncate_legend=40,
)

# Temporal quartile scatter series — size encodes temporal recency (7→10)
chart.add("Hours 1–100", early, stroke=False, dots_size=7)
chart.add("Hours 101–200", mid_early, stroke=False, dots_size=8)
chart.add("Hours 201–300", mid_late, stroke=False, dots_size=9)
chart.add("Hours 301–399", late, stroke=False, dots_size=10)

# ±1σ spread envelope (structural layer — no legend entry)
env_style = {"width": 3, "dasharray": "6, 8", "linecap": "round"}
chart.add(None, upper_env, stroke=True, show_dots=False, stroke_style=env_style)
chart.add(None, lower_env, stroke=True, show_dots=False, stroke_style=env_style)

# Diagonal reference line y = x (strong autocorrelation aligns points to this)
chart.add(
    "y = x (±1σ)",
    ref_line,
    stroke=True,
    show_dots=False,
    stroke_style={"width": 6, "dasharray": "24, 12", "linecap": "round"},
)

# Save — PNG + interactive HTML (pygal is interactive)
chart.render_to_png(f"plot-{THEME}.png")
with open(f"plot-{THEME}.html", "wb") as f:
    f.write(chart.render())

Part of Lag Plot for Time Series Autocorrelation Diagnosis on anyplot.ai.

Other implementations