import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from statsmodels.tsa.stattools import acf, pacf
BLUE, GREY = "#2E6FDB", "#4A4A4A"
def add_corr_plot(fig, values, conf, lags, row, col, title):
"""Stem plot + shaded confidence band, like statsmodels' plot_acf/plot_pacf.
values: correlation per lag; conf: (n_lags, 2) CI already centred at 0."""
# Confidence band (shaded area)
fig.add_trace(go.Scatter(x=lags, y=conf[:, 1], mode="lines",
line=dict(width=0), hoverinfo="skip",
showlegend=False), row=row, col=col)
fig.add_trace(go.Scatter(x=lags, y=conf[:, 0], mode="lines",
line=dict(width=0), fill="tonexty",
fillcolor="rgba(46,111,219,0.15)",
hoverinfo="skip", showlegend=False),
row=row, col=col)
# Stems (thin bars) + dots on top
fig.add_trace(go.Bar(x=lags, y=values, width=0.08, marker_color=BLUE,
showlegend=False, hoverinfo="skip"), row=row, col=col)
fig.add_trace(go.Scatter(x=lags, y=values, mode="markers",
marker=dict(color=BLUE, size=8),
showlegend=False,
hovertemplate="lag %{x}<br>%{y:.3f}<extra></extra>"),
row=row, col=col)
fig.update_xaxes(title_text="Lag", showgrid=False, zeroline=False,
dtick=5, row=row, col=col)
fig.update_yaxes(title_text=title, range=[-1.05, 1.05], gridcolor="#F0F0F0",
zerolinecolor="#999999", row=row, col=col)
def plotly_acf_pacf(y, nlags=20, alpha=0.05, title=None, side_by_side = True, height = 210, width = 950):
n = len(y)
lags = np.arange(nlags + 1)
# ACF with Bartlett confidence intervals (centred at zero, as in statsmodels)
acf_vals, acf_ci = acf(y, nlags=nlags, alpha=alpha, fft=False)
acf_ci = acf_ci - acf_vals[:, None]
# PACF with the usual +/- z / sqrt(n) band
pacf_vals = pacf(y, nlags=nlags, method="ywm")
z = 1.959963984540054 # norm.ppf(1 - alpha/2) for alpha = 0.05
band = z / np.sqrt(n)
pacf_ci = np.column_stack([-band * np.ones(nlags + 1),
band * np.ones(nlags + 1)])
if side_by_side:
fig = make_subplots(rows=1, cols=2, horizontal_spacing=0.08,
subplot_titles=("Autocorrelation (ACF)",
"Partial Autocorrelation (PACF)"))
add_corr_plot(fig, acf_vals, acf_ci, lags, 1, 1, f"ACF of {title}")
add_corr_plot(fig, pacf_vals, pacf_ci, lags, 1, 2, f"PACF of {title}")
else:
fig = make_subplots(rows=2, cols=1, horizontal_spacing=0.08,
subplot_titles=("Autocorrelation (ACF)",
"Partial Autocorrelation (PACF)"))
add_corr_plot(fig, acf_vals, acf_ci, lags, 1, 1, f"ACF of {title}")
add_corr_plot(fig, pacf_vals, pacf_ci, lags, 2, 1, f"PACF of {title}")
fig.update_layout(template="plotly_white", paper_bgcolor="white",
plot_bgcolor="white", height=height, width=width,
font=dict(family="Helvetica, Arial, sans-serif",
color=GREY),
margin=dict(l=50, r=30, t=20, b=20))
for a in fig.layout.annotations:
a.font = dict(size=14, color=GREY)
return fig
fig_pm25 = plotly_acf_pacf(df['pm2_5'], nlags=25, alpha=0.05, title='PM 2.5 (µg/m³)')
fig_pm25.show()