"""Historical decomposition plotting."""
from typing import TYPE_CHECKING
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
if TYPE_CHECKING:
from impulso.results import HistoricalDecompositionResult
[docs]
def plot_historical_decomposition(
result: "HistoricalDecompositionResult",
figsize: tuple[float, float] | None = None,
) -> Figure:
"""Plot historical decomposition as stacked bar charts.
One panel per response variable, showing the propagated contribution of
each structural shock over time, with the posterior median of the total
deviation from the deterministic baseline overlaid as a line. The line
is the median of the per-draw sum over shocks, so it matches
`data - result.baseline()` exactly; because median-of-sum differs from
sum-of-medians, it need not exactly top the median bars. Shock labels
come from the result's `shock` coordinate, so partially-identified
decompositions (with an `unidentified_remainder` column) render
correctly.
Args:
result: HistoricalDecompositionResult.
figsize: Figure size. Defaults to (12, 3 * n_vars).
Returns:
Matplotlib Figure.
"""
med = result.median()
deviation = result.deviation()
shock_names = result.shock_names
n_vars = len(result.var_names)
T = len(med.index)
if figsize is None:
figsize = (12, 3 * n_vars)
fig, axes = plt.subplots(n_vars, 1, figsize=figsize, sharex=True)
if n_vars == 1:
axes = [axes]
fig.suptitle("Historical Decomposition")
time_idx = range(T)
for i, resp in enumerate(result.var_names):
panel = med[resp].values # (T, n_shocks)
bottom_pos = None
bottom_neg = None
for j, shock in enumerate(shock_names):
vals = panel[:, j]
pos = vals.clip(min=0)
neg = vals.clip(max=0)
if bottom_pos is None:
axes[i].bar(time_idx, pos, width=1.0, label=shock, alpha=0.8)
axes[i].bar(time_idx, neg, width=1.0, alpha=0.8, color=f"C{j}")
bottom_pos = pos.copy()
bottom_neg = neg.copy()
else:
axes[i].bar(time_idx, pos, width=1.0, bottom=bottom_pos, label=shock, alpha=0.8)
axes[i].bar(
time_idx,
neg,
width=1.0,
bottom=bottom_neg,
alpha=0.8,
color=f"C{j}",
)
bottom_pos += pos
bottom_neg += neg
axes[i].plot(
time_idx,
deviation[resp].values,
color="black",
linewidth=1.1,
label="deviation from baseline",
)
axes[i].set_ylabel(resp)
axes[i].axhline(0, color="0.5", linewidth=0.5, linestyle="--")
if i == 0:
axes[i].legend(fontsize=8, loc="upper right")
axes[-1].set_xlabel("Time")
fig.tight_layout()
return fig