Autocorrelation and Partial Autocorrelation (ACF/PACF) Plot — Bokeh

Displays the autocorrelation function (ACF) and partial autocorrelation function (PACF) of a time series as vertical stem/bar plots arranged in two vertically stacked subplots. Each lag is represented by a vertical line from zero to the correlation value, with horizontal dashed lines indicating 95% confidence bounds. These plots are essential for identifying the order of AR and MA components in ARIMA modeling and for diagnosing residual independence.

Autocorrelation and Partial Autocorrelation (ACF/PACF) Plot rendered with Bokeh

Python source (Bokeh)

""" anyplot.ai
acf-pacf: Autocorrelation and Partial Autocorrelation (ACF/PACF) Plot
Library: bokeh 3.9.1 | Python 3.13.13
Quality: 90/100 | Updated: 2026-06-10
"""

import os
import sys


# Prevent self-import: this file is named bokeh.py, which shadows the installed
# bokeh package when its directory sits at the front of sys.path.
_this_dir = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p or ".") != _this_dir]

import time
from pathlib import Path

import numpy as np
from bokeh.io import output_file, save
from bokeh.layouts import column
from bokeh.models import BoxAnnotation, ColumnDataSource, HoverTool, Label, Span
from bokeh.plotting import figure
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from statsmodels.tsa.stattools import acf, pacf


# Theme-adaptive chrome (Imprint palette)
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 palette — first series always #009E73
BRAND = "#009E73"  # significant lags (Imprint position 1)
CI_COLOR = "#AE3030"  # confidence interval (Imprint position 5 — semantic threshold)
AR_ACCENT = "#BD8233"  # AR(2) highlights (Imprint position 4, ochre)

# Data — simulated monthly retail sales with AR(2) structure
# AR(2): positive lag-1 momentum + negative lag-2 correction
np.random.seed(42)
n_obs = 200
series = np.zeros(n_obs)
for i in range(2, n_obs):
    series[i] = 0.6 * series[i - 1] - 0.3 * series[i - 2] + np.random.randn()

n_lags = 35
acf_values = acf(series, nlags=n_lags, fft=True)
pacf_values = pacf(series, nlags=n_lags, method="ywm")
conf_bound = 1.96 / np.sqrt(n_obs)

acf_significant = np.abs(acf_values) > conf_bound
pacf_significant = np.abs(pacf_values[1:]) > conf_bound

# Canvas — two stacked subplots; Selenium screenshots viewport at W×H = 3200×1800
W, H = 3200, 1800
SUBPLOT_H = 880

# ACF data sources
acf_lags = np.arange(len(acf_values))
acf_colors = [BRAND if s else INK_MUTED for s in acf_significant]
acf_stem_src = ColumnDataSource(
    {"x0": acf_lags, "y0": np.zeros(len(acf_lags)), "x1": acf_lags, "y1": acf_values, "color": acf_colors}
)
acf_src = ColumnDataSource(
    {
        "x": acf_lags,
        "y": acf_values,
        "color": acf_colors,
        "sig": ["Significant" if s else "Not significant" for s in acf_significant],
        "val": [f"{v:.3f}" for v in acf_values],
    }
)

# PACF data sources
pacf_lags = np.arange(1, len(pacf_values))
pacf_vals = pacf_values[1:]
pacf_colors = [BRAND if s else INK_MUTED for s in pacf_significant]
pacf_stem_src = ColumnDataSource(
    {"x0": pacf_lags, "y0": np.zeros(len(pacf_lags)), "x1": pacf_lags, "y1": pacf_vals, "color": pacf_colors}
)
pacf_src = ColumnDataSource(
    {
        "x": pacf_lags,
        "y": pacf_vals,
        "color": pacf_colors,
        "sig": ["Significant" if s else "Not significant" for s in pacf_significant],
        "val": [f"{v:.3f}" for v in pacf_vals],
    }
)

# --- ACF plot (top) ---
p_acf = figure(
    title="acf-pacf · bokeh · anyplot.ai",
    x_axis_label="Lag",
    y_axis_label="ACF",
    width=W,
    height=SUBPLOT_H,
    background_fill_color=PAGE_BG,
    border_fill_color=PAGE_BG,
    toolbar_location=None,
    min_border_bottom=140,
    min_border_left=180,
    min_border_top=110,
    min_border_right=50,
)

p_acf.segment("x0", "y0", "x1", "y1", source=acf_stem_src, line_width=5, color="color", alpha=0.85)
p_acf.scatter("x", "y", source=acf_src, size=12, color="color", alpha=0.9)

p_acf.add_layout(BoxAnnotation(bottom=-conf_bound, top=conf_bound, fill_alpha=0.08, fill_color=CI_COLOR, line_alpha=0))
p_acf.add_layout(
    Span(
        location=conf_bound, dimension="width", line_dash="dashed", line_width=2.5, line_color=CI_COLOR, line_alpha=0.7
    )
)
p_acf.add_layout(
    Span(
        location=-conf_bound, dimension="width", line_dash="dashed", line_width=2.5, line_color=CI_COLOR, line_alpha=0.7
    )
)
p_acf.add_layout(Span(location=0, dimension="width", line_width=1.5, line_color=INK_SOFT, line_alpha=0.5))

p_acf.add_tools(HoverTool(tooltips=[("Lag", "@x"), ("ACF", "@val"), ("Status", "@sig")], mode="vline"))

# --- PACF plot (bottom) ---
p_pacf = figure(
    x_axis_label="Lag",
    y_axis_label="PACF",
    x_range=p_acf.x_range,
    width=W,
    height=SUBPLOT_H,
    background_fill_color=PAGE_BG,
    border_fill_color=PAGE_BG,
    toolbar_location=None,
    min_border_bottom=160,
    min_border_left=180,
    min_border_top=50,
    min_border_right=50,
)

p_pacf.segment("x0", "y0", "x1", "y1", source=pacf_stem_src, line_width=5, color="color", alpha=0.85)
p_pacf.scatter("x", "y", source=pacf_src, size=12, color="color", alpha=0.9)

p_pacf.add_layout(BoxAnnotation(bottom=-conf_bound, top=conf_bound, fill_alpha=0.08, fill_color=CI_COLOR, line_alpha=0))
p_pacf.add_layout(
    Span(
        location=conf_bound, dimension="width", line_dash="dashed", line_width=2.5, line_color=CI_COLOR, line_alpha=0.7
    )
)
p_pacf.add_layout(
    Span(
        location=-conf_bound, dimension="width", line_dash="dashed", line_width=2.5, line_color=CI_COLOR, line_alpha=0.7
    )
)
p_pacf.add_layout(Span(location=0, dimension="width", line_width=1.5, line_color=INK_SOFT, line_alpha=0.5))

# AR(2) structural lags highlighted in ochre; annotation placed edge-right to avoid data overlap
ar_lags = [1, 2]
ar_vals = [pacf_values[lag] for lag in ar_lags]
p_pacf.scatter(ar_lags, ar_vals, size=22, color=AR_ACCENT, alpha=0.95, line_color=INK, line_width=2)

p_pacf.add_layout(
    Label(
        x=3,
        y=float(pacf_values[1]),
        text="AR(2) identified",
        text_font_size="28pt",
        text_color=AR_ACCENT,
        text_font_style="bold",
        x_offset=5,
        y_offset=-5,
    )
)

p_pacf.add_tools(HoverTool(tooltips=[("Lag", "@x"), ("PACF", "@val"), ("Status", "@sig")], mode="vline"))

# Apply canonical bokeh font sizes and theme-adaptive chrome to both subplots
for p in [p_acf, p_pacf]:
    p.title.text_font_size = "50pt"
    p.title.text_color = INK
    p.xaxis.axis_label_text_font_size = "42pt"
    p.yaxis.axis_label_text_font_size = "42pt"
    p.xaxis.major_label_text_font_size = "34pt"
    p.yaxis.major_label_text_font_size = "34pt"
    p.xaxis.axis_label_text_color = INK
    p.yaxis.axis_label_text_color = INK
    p.xaxis.major_label_text_color = INK_SOFT
    p.yaxis.major_label_text_color = INK_SOFT
    p.xaxis.axis_line_color = INK_SOFT
    p.yaxis.axis_line_color = INK_SOFT
    p.xaxis.major_tick_line_color = INK_SOFT
    p.yaxis.major_tick_line_color = INK_SOFT
    p.xaxis.minor_tick_line_color = None
    p.yaxis.minor_tick_line_color = None
    p.xgrid.grid_line_color = INK
    p.xgrid.grid_line_alpha = 0
    p.ygrid.grid_line_color = INK
    p.ygrid.grid_line_alpha = 0.15
    p.outline_line_color = INK_SOFT

# Layout — two stacked subplots with minimal gap
layout = column(p_acf, p_pacf, spacing=5)

# Save interactive HTML
output_file(f"plot-{THEME}.html")
save(layout)

# Screenshot via Selenium headless Chrome — matches bokeh.md pattern
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H}",
    "--hide-scrollbars",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
# CDP override forces an exact W×H viewport regardless of outer window chrome
driver.execute_cdp_cmd(
    "Emulation.setDeviceMetricsOverride", {"width": W, "height": H, "deviceScaleFactor": 1, "mobile": False}
)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
time.sleep(3)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

Part of Autocorrelation and Partial Autocorrelation (ACF/PACF) Plot on anyplot.ai.

Other implementations