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

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 plotnine

Python source (plotnine)

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

import os
import sys

import numpy as np
import pandas as pd


# Work around naming conflict with plotnine.py script and plotnine package
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(".")

from plotnine import (
    aes,
    element_blank,
    element_line,
    element_rect,
    element_text,
    facet_wrap,
    geom_hline,
    geom_point,
    geom_segment,
    geom_vline,
    ggplot,
    guides,
    labs,
    scale_color_manual,
    scale_x_continuous,
    scale_y_continuous,
    theme,
    theme_minimal,
)
from statsmodels.tsa.stattools import acf, pacf


# 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"

# Imprint palette positions used
BRAND = "#009E73"  # position 1 — significant lags (brand green, first series)
ALARM = "#AE3030"  # position 5 — confidence bounds (semantic: alert/threshold)

# Data — simulated monthly temperature with seasonality and AR(1) component
np.random.seed(42)
n_obs = 240
time = np.arange(n_obs)
seasonal = 12 * np.sin(2 * np.pi * time / 12) + 4 * np.cos(2 * np.pi * time / 6)
ar_component = np.zeros(n_obs)
for t in range(1, n_obs):
    ar_component[t] = 0.4 * ar_component[t - 1] + np.random.normal(0, 2)
temperature = seasonal + ar_component

# Compute ACF and PACF
n_lags = 36
acf_values = acf(temperature, nlags=n_lags, fft=True)
pacf_values = pacf(temperature, nlags=n_lags, method="ywm")
confidence_bound = 1.96 / np.sqrt(n_obs)

# Build long-format DataFrame for faceting
acf_df = pd.DataFrame({"lag": np.arange(len(acf_values)), "correlation": acf_values, "panel": "ACF"})
pacf_df = pd.DataFrame({"lag": np.arange(1, len(pacf_values)), "correlation": pacf_values[1:], "panel": "PACF"})
df = pd.concat([acf_df, pacf_df], ignore_index=True)
df["panel"] = pd.Categorical(df["panel"], categories=["ACF", "PACF"], ordered=True)

# Mark significance: lags outside confidence bounds
df["significant"] = np.where(np.abs(df["correlation"]) > confidence_bound, "Significant", "Non-significant")
# Lag 0 in ACF is always 1.0 by definition — not a meaningful significant lag
df.loc[(df["panel"] == "ACF") & (df["lag"] == 0), "significant"] = "Non-significant"

# Seasonal lag markers restricted to ACF panel — period-12 structure at lags 12, 24, 36
seasonal_ann_df = pd.DataFrame(
    {
        "xintercept": [12, 24, 36],
        "panel": pd.Categorical(["ACF", "ACF", "ACF"], categories=["ACF", "PACF"], ordered=True),
    }
)

# Title — 41 chars, within 67-char baseline, no font scaling needed
title = "acf-pacf · python · plotnine · anyplot.ai"

# Plot — strip labels "ACF" / "PACF" serve as per-panel y-axis identifiers per spec
plot = (
    ggplot(df, aes(x="lag", y="correlation", color="significant"))
    + geom_hline(yintercept=0, color=INK_SOFT, size=0.6, alpha=0.8)
    + geom_vline(
        data=seasonal_ann_df, mapping=aes(xintercept="xintercept"), color=BRAND, alpha=0.14, size=0.8, linetype="dotted"
    )
    + geom_hline(yintercept=confidence_bound, linetype="dashed", color=ALARM, size=0.7, alpha=0.65)
    + geom_hline(yintercept=-confidence_bound, linetype="dashed", color=ALARM, size=0.7, alpha=0.65)
    + geom_segment(aes(x="lag", xend="lag", y=0, yend="correlation"), size=1.2)
    + geom_point(size=3.0)
    + scale_color_manual(values={"Significant": BRAND, "Non-significant": INK_MUTED})
    + guides(color="none")
    + facet_wrap("~panel", ncol=1, scales="free_y")
    + scale_x_continuous(breaks=list(range(0, n_lags + 1, 6)))
    + scale_y_continuous(expand=(0.04, 0))
    + labs(x="Lag", y="", title=title)
    + theme_minimal()
    + theme(
        figure_size=(8, 4.5),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG),
        panel_grid_major_x=element_blank(),
        panel_grid_minor_x=element_blank(),
        panel_grid_major_y=element_line(color=INK, size=0.2, alpha=0.12),
        panel_grid_minor_y=element_blank(),
        axis_title_x=element_text(color=INK, size=10),
        axis_title_y=element_blank(),
        axis_text=element_text(color=INK_SOFT, size=8),
        plot_title=element_text(color=INK, size=12, face="bold"),
        strip_background=element_rect(fill=PAGE_BG, color="none"),
        strip_text=element_text(color=INK, size=10, face="bold"),
        panel_spacing_y=0.08,
    )
)

# Save — canvas: 8×4.5 in × 400 dpi = 3200×1800 px
plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False)

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

Other implementations