A time series plot that displays historical observed data followed by a forecast projection with confidence intervals or uncertainty bands. The plot clearly distinguishes between the historical period and the forecast period using a vertical line marker, with shaded bands representing different confidence levels (typically 80% and 95%). This visualization is essential for communicating prediction uncertainty in forecasting applications, helping stakeholders understand both the expected values and the range of possible outcomes.

""" anyplot.ai
timeseries-forecast-uncertainty: Time Series Forecast with Uncertainty Band
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 91/100 | Updated: 2026-05-19
"""
import os
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib.lines import Line2D
from matplotlib.patches import Patch
# Theme tokens
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"
# Okabe-Ito palette
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477"]
COLOR_HISTORICAL = IMPRINT[0]
COLOR_FORECAST = IMPRINT[1]
# Higher alpha in dark mode — orange over near-black otherwise looks brownish
ALPHA_95 = 0.30 if THEME == "dark" else 0.22
ALPHA_80 = 0.42 if THEME == "dark" else 0.30
np.random.seed(42)
# Data — stock price with ~3-month history and 4-week forecast
n_historical = 60
n_forecast = 20
dates = pd.date_range(start="2025-01-01", periods=n_historical + n_forecast, freq="B")
t = np.arange(n_historical)
historical_prices = 150 + 0.15 * t + 2.5 * np.sin(2 * np.pi * t / 20) + np.random.normal(0, 1.5, n_historical)
t_fc = np.arange(n_historical, n_historical + n_forecast)
forecast_prices = 150 + 0.15 * t_fc + 2.5 * np.sin(2 * np.pi * t_fc / 20)
horizon = np.arange(1, n_forecast + 1)
std_growth = 1.5 * np.sqrt(horizon)
lower_95 = forecast_prices - 1.96 * std_growth
upper_95 = forecast_prices + 1.96 * std_growth
lower_80 = forecast_prices - 1.28 * std_growth
upper_80 = forecast_prices + 1.28 * std_growth
# Wide-form for CI bands; long-form for seaborn's data-aware lineplot
df_wide = pd.DataFrame(
{
"date": dates,
"actual": list(historical_prices) + [np.nan] * n_forecast,
"forecast": [np.nan] * (n_historical - 1) + [historical_prices[-1]] + list(forecast_prices),
"lower_80": [np.nan] * (n_historical - 1) + [historical_prices[-1]] + list(lower_80),
"upper_80": [np.nan] * (n_historical - 1) + [historical_prices[-1]] + list(upper_80),
"lower_95": [np.nan] * (n_historical - 1) + [historical_prices[-1]] + list(lower_95),
"upper_95": [np.nan] * (n_historical - 1) + [historical_prices[-1]] + list(upper_95),
}
)
long_data = pd.concat(
[
df_wide[["date", "actual"]].rename(columns={"actual": "price"}).assign(series="Historical"),
df_wide[["date", "forecast"]].rename(columns={"forecast": "price"}).assign(series="Forecast"),
]
).dropna()
# Configure seaborn theme
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.10,
"legend.facecolor": ELEVATED_BG,
"legend.edgecolor": INK_SOFT,
},
)
# Plot
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)
# Subtle forecast-region shading to visually separate forecast from history
ax.axvspan(dates[n_historical - 1], dates[-1], alpha=0.04, color=INK, zorder=0)
# Confidence interval bands (95% outermost/lightest, 80% inner/more opaque — nested)
ax.fill_between(df_wide["date"], df_wide["lower_95"], df_wide["upper_95"], alpha=ALPHA_95, color=COLOR_FORECAST)
ax.fill_between(df_wide["date"], df_wide["lower_80"], df_wide["upper_80"], alpha=ALPHA_80, color=COLOR_FORECAST)
# Seaborn lineplot — idiomatic long-form API with hue + style + dashes
sns.lineplot(
data=long_data,
x="date",
y="price",
hue="series",
style="series",
palette={"Historical": COLOR_HISTORICAL, "Forecast": COLOR_FORECAST},
dashes={"Historical": (1, 0), "Forecast": (6, 2)},
linewidth=3,
ax=ax,
legend=False,
)
# Forecast boundary marker
ax.axvline(x=dates[n_historical - 1], color=INK_SOFT, linestyle=":", linewidth=1.5, alpha=0.5)
ax.text(
dates[n_historical - 1],
0.97,
" Forecast →",
transform=ax.get_xaxis_transform(),
color=INK_SOFT,
fontsize=8,
va="top",
)
# Style — title at 11pt, axes at 10pt for clear typographic hierarchy
ax.set_title(
"timeseries-forecast-uncertainty · python · seaborn · anyplot.ai",
fontsize=11,
fontweight="medium",
color=INK,
pad=8,
)
ax.set_xlabel("Date", fontsize=10, color=INK)
ax.set_ylabel("Stock Price ($)", fontsize=10, color=INK)
ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT)
sns.despine(ax=ax)
ax.spines["left"].set_color(INK_SOFT)
ax.spines["bottom"].set_color(INK_SOFT)
ax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)
ax.set_axisbelow(True)
# Combined legend: line handles + CI band patches
legend_elements = [
Line2D([0], [0], color=COLOR_HISTORICAL, linewidth=3, label="Historical"),
Line2D([0], [0], color=COLOR_FORECAST, linewidth=3, linestyle=(0, (6, 2)), label="Forecast"),
Patch(facecolor=COLOR_FORECAST, alpha=ALPHA_80, label="80% Confidence"),
Patch(facecolor=COLOR_FORECAST, alpha=ALPHA_95, label="95% Confidence"),
]
ax.legend(handles=legend_elements, fontsize=8, loc="upper left", framealpha=1.0, fancybox=False, edgecolor=INK_SOFT)
plt.tight_layout()
# Save
plt.savefig(f"plot-{THEME}.png", dpi=400, bbox_inches="tight", facecolor=PAGE_BG)
Part of Time Series Forecast with Uncertainty Band on anyplot.ai.