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

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 Seaborn

Python source (Seaborn)

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

import os
import sys


# Prevent local files (matplotlib.py, etc.) from shadowing installed packages
_script_dir = os.path.dirname(os.path.abspath(__file__))
if _script_dir in sys.path:
    sys.path.remove(_script_dir)
if "" in sys.path:
    sys.path.remove("")
if "." in sys.path:
    sys.path.remove(".")

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns


# Theme tokens (Imprint chrome — see prompts/default-style-guide.md)
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 — always first series
ANYPLOT_AMBER = "#DDCC77"  # caution/threshold marker for CI bounds

sns.set_theme(
    style="ticks",
    rc={
        "figure.facecolor": PAGE_BG,
        "axes.facecolor": PAGE_BG,
        "axes.edgecolor": INK_SOFT,
        "axes.labelcolor": INK,
        "text.color": INK,
        "xtick.color": INK_SOFT,
        "ytick.color": INK_SOFT,
        "grid.color": INK,
        "grid.alpha": 0.15,
        "legend.facecolor": ELEVATED_BG,
        "legend.edgecolor": INK_SOFT,
    },
)

# Data: ARMA(1,1) process with seasonal component (airline passenger residuals)
np.random.seed(42)
n_obs = 200
ar1_coeff = 0.7
ma1_coeff = 0.4
seasonal_period = 12
seasonal_strength = 0.3
noise = np.random.randn(n_obs)
series = np.zeros(n_obs)
series[0] = noise[0]
for t in range(1, n_obs):
    seasonal = seasonal_strength * np.sin(2 * np.pi * t / seasonal_period)
    series[t] = ar1_coeff * series[t - 1] + noise[t] + ma1_coeff * noise[t - 1] + seasonal

# Compute ACF
n_lags = 35
mean = np.mean(series)
var = np.sum((series - mean) ** 2)
acf_values = np.array([np.sum((series[: n_obs - k] - mean) * (series[k:] - mean)) / var for k in range(n_lags + 1)])

# Compute PACF via Durbin-Levinson recursion
pacf_values = np.zeros(n_lags + 1)
pacf_values[0] = 1.0
pacf_values[1] = acf_values[1]
phi = np.zeros((n_lags + 1, n_lags + 1))
phi[1, 1] = acf_values[1]
for k in range(2, n_lags + 1):
    num = acf_values[k] - np.sum(phi[k - 1, 1:k] * acf_values[k - 1 : 0 : -1])
    den = 1.0 - np.sum(phi[k - 1, 1:k] * acf_values[1:k])
    phi[k, k] = num / den if den != 0 else 0
    for j in range(1, k):
        phi[k, j] = phi[k - 1, j] - phi[k, k] * phi[k - 1, k - j]
    pacf_values[k] = phi[k, k]

lags_acf = np.arange(0, n_lags + 1)
lags_pacf = np.arange(1, n_lags + 1)
conf_bound = 1.96 / np.sqrt(n_obs)

# DataFrames with significance classification for seaborn hue encoding
acf_df = pd.DataFrame(
    {
        "Lag": lags_acf,
        "Correlation": acf_values,
        "Significance": np.where((np.abs(acf_values) > conf_bound) | (lags_acf == 0), "Significant", "Within CI"),
    }
)
pacf_df = pd.DataFrame(
    {
        "Lag": lags_pacf,
        "Correlation": pacf_values[1:],
        "Significance": np.where(np.abs(pacf_values[1:]) > conf_bound, "Significant", "Within CI"),
    }
)

sig_palette = {"Significant": BRAND, "Within CI": INK_MUTED}


def make_stem_df(df):
    # Paired-row format required by sns.lineplot(units='Lag') to draw each stem as an
    # individual vertical segment without cross-lag interpolation.
    rows = []
    for _, row in df.iterrows():
        rows.append({"Lag": row["Lag"], "y": 0.0, "Significance": row["Significance"]})
        rows.append({"Lag": row["Lag"], "y": row["Correlation"], "Significance": row["Significance"]})
    return pd.DataFrame(rows)


acf_stem_df = make_stem_df(acf_df)
pacf_stem_df = make_stem_df(pacf_df)

# Canvas: figsize=(8, 4.5) @ dpi=400 → exactly 3200×1800 px (landscape 16:9)
fig, (ax_acf, ax_pacf) = plt.subplots(2, 1, figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG, sharex=True)
ax_acf.set_facecolor(PAGE_BG)
ax_pacf.set_facecolor(PAGE_BG)

# ACF: sns.lineplot with units='Lag' draws each significance-colored stem as a
# discrete vertical segment — idiomatic seaborn statistical unit rendering
sns.lineplot(
    data=acf_stem_df,
    x="Lag",
    y="y",
    hue="Significance",
    palette=sig_palette,
    units="Lag",
    estimator=None,
    linewidth=1.8,
    ax=ax_acf,
    legend=False,
)
sns.scatterplot(
    data=acf_df,
    x="Lag",
    y="Correlation",
    hue="Significance",
    hue_order=["Significant", "Within CI"],
    palette=sig_palette,
    s=55,
    zorder=5,
    edgecolor=PAGE_BG,
    linewidth=0.5,
    ax=ax_acf,
    legend=True,
)

# PACF: same seaborn approach from lag 1
sns.lineplot(
    data=pacf_stem_df,
    x="Lag",
    y="y",
    hue="Significance",
    palette=sig_palette,
    units="Lag",
    estimator=None,
    linewidth=1.8,
    ax=ax_pacf,
    legend=False,
)
sns.scatterplot(
    data=pacf_df,
    x="Lag",
    y="Correlation",
    hue="Significance",
    palette=sig_palette,
    s=55,
    zorder=5,
    edgecolor=PAGE_BG,
    linewidth=0.5,
    ax=ax_pacf,
    legend=False,
)

# CI bounds, baseline, and grid for both panels
for ax in (ax_acf, ax_pacf):
    ax.axhline(y=0, color=INK_SOFT, linewidth=0.8)
    ax.axhline(y=conf_bound, color=ANYPLOT_AMBER, linestyle="--", linewidth=1.5, alpha=0.9)
    ax.axhline(y=-conf_bound, color=ANYPLOT_AMBER, linestyle="--", linewidth=1.5, alpha=0.9)
    ax.fill_between([-0.5, n_lags + 0.5], -conf_bound, conf_bound, color=ANYPLOT_AMBER, alpha=0.07)
    ax.set_xlim(-0.5, n_lags + 0.5)
    ax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)
    ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT)

# Spine styling
sns.despine(fig=fig)
for ax in (ax_acf, ax_pacf):
    ax.spines["left"].set_color(INK_SOFT)
    ax.spines["bottom"].set_color(INK_SOFT)

# Axis labels
ax_acf.set_ylabel("ACF", fontsize=10, color=INK)
ax_pacf.set_ylabel("PACF", fontsize=10, color=INK)
ax_pacf.set_xlabel("Lag", fontsize=10, color=INK)

# X-ticks every 5 lags (shared axis — set once on either panel)
ax_pacf.set_xticks(np.arange(0, n_lags + 1, 5))

# Legend in ACF panel: seaborn auto-generates Significant/Within CI handles from
# scatterplot hue; extend with the amber CI dashed-line handle
handles, labels = ax_acf.get_legend_handles_labels()
ci_handle = plt.Line2D([0], [0], linestyle="--", color=ANYPLOT_AMBER, linewidth=1.5)
handles.append(ci_handle)
labels.append("95% CI")
ax_acf.legend(handles=handles, labels=labels, loc="upper right", fontsize=8, facecolor=ELEVATED_BG, edgecolor=INK_SOFT)

# Data storytelling annotations — AR(1) signature visible in both panels
ax_acf.annotate(
    "Gradual decay → AR process",
    xy=(4, acf_values[4]),
    xytext=(13, 0.58),
    fontsize=7,
    color=INK_MUTED,
    arrowprops={"arrowstyle": "->", "color": INK_MUTED, "lw": 0.7},
)
ax_pacf.annotate(
    "Spike at lag 1 → AR(1) order",
    xy=(1, pacf_values[1]),
    xytext=(7, 0.63),
    fontsize=7,
    color=INK_MUTED,
    arrowprops={"arrowstyle": "->", "color": INK_MUTED, "lw": 0.7},
)

# Title — "acf-pacf · python · seaborn · anyplot.ai" is 40 chars (< 67 baseline → fontsize=12)
title = "acf-pacf · python · seaborn · anyplot.ai"
fig.suptitle(title, fontsize=12, fontweight="medium", color=INK, y=0.99)
fig.subplots_adjust(top=0.92, bottom=0.13, hspace=0.3)

# Save — no bbox_inches to preserve exact 3200×1800 canvas
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

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

Other implementations