Lag Plot for Time Series Autocorrelation Diagnosis — Altair

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 Altair

Python source (Altair)

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

import os

import altair as alt
import numpy as np
import pandas as pd
from PIL import Image


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

# Data — synthetic AR(1) process with strong positive autocorrelation
np.random.seed(42)
n_points = 500
lag = 1
phi = 0.85
noise = np.random.normal(0, 1, n_points)
values = np.zeros(n_points)
values[0] = noise[0]
for i in range(1, n_points):
    values[i] = phi * values[i - 1] + noise[i]

y_t = values[:-lag]
y_t_lag = values[lag:]
r_value = np.corrcoef(y_t, y_t_lag)[0, 1]

df = pd.DataFrame({"y_t": y_t, "y_t_lag": y_t_lag, "time_index": np.arange(n_points - lag)})

# Axis domain with margin
margin = 0.4
axis_min = min(df["y_t"].min(), df["y_t_lag"].min()) - margin
axis_max = max(df["y_t"].max(), df["y_t_lag"].max()) + margin
domain = [axis_min, axis_max]

# Reference line (y = x diagonal — perfect persistence baseline)
ref_df = pd.DataFrame({"x": [axis_min, axis_max], "y": [axis_min, axis_max]})

# Correlation annotation (top-left, away from the dense cluster)
annot_df = pd.DataFrame({"x": [axis_min + 0.25], "y": [axis_max - 0.3], "label": [f"r = {r_value:.3f}"]})

title_str = "scatter-lag · python · altair · anyplot.ai"
subtitle_str = f"AR(1) process (φ = {phi}) | lag = {lag} | n = {n_points - lag}"

# Chart layers
reference_line = (
    alt.Chart(ref_df).mark_line(strokeDash=[8, 6], strokeWidth=1.5, color=INK_MUTED).encode(x="x:Q", y="y:Q")
)

points = (
    alt.Chart(df)
    .mark_point(size=40, filled=True, strokeWidth=0.5, stroke=PAGE_BG, opacity=0.5)
    .encode(
        x=alt.X("y_t:Q", title="y(t)", scale=alt.Scale(domain=domain), axis=alt.Axis(tickCount=8)),
        y=alt.Y("y_t_lag:Q", title="y(t + 1)", scale=alt.Scale(domain=domain), axis=alt.Axis(tickCount=8)),
        # Imprint sequential colormap: brand green → blue (single-polarity continuous)
        color=alt.Color(
            "time_index:Q",
            scale=alt.Scale(range=["#009E73", "#4467A3"]),
            legend=alt.Legend(
                title="Time Index",
                titleFontSize=10,
                labelFontSize=10,
                gradientLength=180,
                gradientThickness=12,
                orient="right",
                offset=8,
            ),
        ),
        tooltip=[
            alt.Tooltip("y_t:Q", title="y(t)", format=".2f"),
            alt.Tooltip("y_t_lag:Q", title="y(t+1)", format=".2f"),
            alt.Tooltip("time_index:Q", title="Time Index"),
        ],
    )
)

# OLS regression line — slope ≈ φ, contrasts with the y=x diagonal to show autocorrelation strength
regression_line = (
    alt.Chart(df)
    .transform_regression("y_t", "y_t_lag", method="linear")
    .mark_line(strokeWidth=2.5, color=BRAND, opacity=0.85)
    .encode(x="y_t:Q", y="y_t_lag:Q")
)

annotation = (
    alt.Chart(annot_df)
    .mark_text(align="left", baseline="top", fontSize=12, fontWeight="bold", color=INK)
    .encode(x="x:Q", y="y:Q", text="label:N")
)

chart = (
    (reference_line + points + regression_line + annotation)
    .properties(
        width=620,
        height=320,
        background=PAGE_BG,
        padding={"left": 0, "right": 0, "top": 0, "bottom": 0},
        title=alt.Title(title_str, fontSize=16, subtitle=subtitle_str, subtitleFontSize=12, subtitleColor=INK_SOFT),
    )
    .configure_view(fill=PAGE_BG, strokeWidth=0, continuousWidth=620, continuousHeight=320)
    .configure_axis(
        labelFontSize=10,
        titleFontSize=12,
        grid=True,
        gridOpacity=0.12,
        gridWidth=0.5,
        gridColor=INK,
        domainWidth=0,
        tickSize=4,
        tickWidth=0.8,
        tickColor=INK_SOFT,
        labelColor=INK_SOFT,
        titleColor=INK,
    )
    .configure_title(anchor="start", offset=12, color=INK)
    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)
)

# Save PNG + HTML
chart.save(f"plot-{THEME}.png", scale_factor=4.0)
chart.save(f"plot-{THEME}.html")

# Pad PNG to exact 3200×1800 (altair canvas hard contract)
TW, TH = 3200, 1800
_img = Image.open(f"plot-{THEME}.png").convert("RGB")
_w, _h = _img.size
if _w > TW or _h > TH:
    raise SystemExit(
        f"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. "
        f"Shrink chart .properties(width=, height=) values and re-render."
    )
if _w < TW or _h < TH:
    _canvas = Image.new("RGB", (TW, TH), PAGE_BG)
    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))
    _canvas.save(f"plot-{THEME}.png")

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

Other implementations