{"spec_id":"timeseries-forecast-uncertainty","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\ntimeseries-forecast-uncertainty: Time Series Forecast with Uncertainty Band\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_line,\n    geom_ribbon,\n    geom_vline,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_color_manual,\n    scale_fill_manual,\n    scale_x_datetime,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Okabe-Ito palette\nIMPRINT = {\"Historical\": \"#009E73\", \"Forecast\": \"#C475FD\"}\n\n# Data - Monthly electricity demand with forecast\nnp.random.seed(42)\n\n# Historical period: 36 months\nn_historical = 36\ndates_historical = pd.date_range(\"2022-01-01\", periods=n_historical, freq=\"MS\")\n\n# Base trend with seasonality\ntrend = np.linspace(100, 130, n_historical)\nseasonality = 15 * np.sin(2 * np.pi * np.arange(n_historical) / 12)\nnoise = np.random.normal(0, 5, n_historical)\nactual_values = trend + seasonality + noise\n\n# Forecast period: 12 months\nn_forecast = 12\ndates_forecast = pd.date_range(dates_historical[-1] + pd.DateOffset(months=1), periods=n_forecast, freq=\"MS\")\n\n# Forecast with expanding uncertainty\nforecast_trend = np.linspace(actual_values[-1], 145, n_forecast)\nforecast_seasonality = 15 * np.sin(2 * np.pi * (np.arange(n_forecast) + n_historical) / 12)\nforecast_values = forecast_trend + forecast_seasonality\n\n# Confidence intervals widen over time\ntime_factor = np.sqrt(np.arange(1, n_forecast + 1))\nci_80 = 5 * time_factor\nci_95 = 10 * time_factor\n\n# Build dataframe for historical data\ndf_historical = pd.DataFrame({\"date\": dates_historical, \"value\": actual_values, \"series\": \"Historical\"})\n\n# Build dataframe for forecast\ndf_forecast = pd.DataFrame({\"date\": dates_forecast, \"value\": forecast_values, \"series\": \"Forecast\"})\n\n# CI band dataframes with label column for legend mapping\ndf_ci_95 = pd.DataFrame(\n    {\"date\": dates_forecast, \"ymin\": forecast_values - ci_95, \"ymax\": forecast_values + ci_95, \"ci\": \"95% CI\"}\n)\ndf_ci_80 = pd.DataFrame(\n    {\"date\": dates_forecast, \"ymin\": forecast_values - ci_80, \"ymax\": forecast_values + ci_80, \"ci\": \"80% CI\"}\n)\n\n# Forecast start date and annotation anchor\nforecast_start = dates_forecast[0]\nannotation_x = forecast_start + pd.DateOffset(months=1)\nannotation_y = float(df_ci_95[\"ymax\"].max()) + 5\n\n# CI fill colors (same hue; alpha per-geom creates visual depth difference)\nCI_COLORS = {\"95% CI\": IMPRINT[\"Forecast\"], \"80% CI\": IMPRINT[\"Forecast\"]}\n\n# Plot\nplot = (\n    ggplot()\n    # 95% confidence band (lighter, outer) — fill mapped for legend entry\n    + geom_ribbon(data=df_ci_95, mapping=aes(x=\"date\", ymin=\"ymin\", ymax=\"ymax\", fill=\"ci\"), alpha=0.20)\n    # 80% confidence band (darker, inner) — fill mapped for legend entry\n    + geom_ribbon(data=df_ci_80, mapping=aes(x=\"date\", ymin=\"ymin\", ymax=\"ymax\", fill=\"ci\"), alpha=0.30)\n    # Vertical line at forecast start\n    + geom_vline(xintercept=forecast_start, linetype=\"dashed\", color=INK_SOFT, size=0.8)\n    # Forecast period label offset right so text clears the vline\n    + annotate(\"text\", x=annotation_x, y=annotation_y, label=\"Forecast period\", color=INK_MUTED, size=9, ha=\"left\")\n    # Historical line\n    + geom_line(data=df_historical, mapping=aes(x=\"date\", y=\"value\", color=\"series\"), size=1.0)\n    # Forecast line (dashed)\n    + geom_line(data=df_forecast, mapping=aes(x=\"date\", y=\"value\", color=\"series\"), size=1.0, linetype=\"dashed\")\n    # Color mapping for lines\n    + scale_color_manual(values=IMPRINT)\n    # Fill mapping for CI bands with legend entries\n    + scale_fill_manual(values=CI_COLORS, name=\"CI bands\")\n    # Suppress \"series\" column header from color legend\n    + guides(color=guide_legend(title=\"\"))\n    # Labels\n    + labs(\n        x=\"Date\",\n        y=\"Electricity Demand (GWh)\",\n        title=\"timeseries-forecast-uncertainty · python · plotnine · anyplot.ai\",\n        subtitle=\"Shaded bands show 80% and 95% confidence intervals; uncertainty widens with forecast horizon\",\n    )\n    # Date axis formatting\n    + scale_x_datetime(date_breaks=\"6 months\", date_labels=\"%b %Y\")\n    # Theme\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_grid_major=element_line(color=INK, size=0.3, alpha=0.1),\n        panel_grid_minor=element_blank(),\n        axis_title=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        axis_text_x=element_text(angle=45, ha=\"right\"),\n        axis_line=element_line(color=INK_SOFT, size=0.5),\n        plot_title=element_text(size=12, color=INK, weight=\"medium\"),\n        plot_subtitle=element_text(size=9, color=INK_MUTED),\n        legend_position=\"top\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=\"none\"),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_title=element_text(size=8, color=INK),\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}