{"spec_id":"heatmap-loss-triangle","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nheatmap-loss-triangle: Actuarial Loss Development Triangle\nLibrary: plotnine 0.15.5 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_text,\n    geom_tile,\n    ggplot,\n    labs,\n    scale_alpha_manual,\n    scale_color_identity,\n    scale_fill_gradient,\n    scale_x_continuous,\n    scale_y_reverse,\n    theme,\n    theme_minimal,\n)\n\n\n# Imprint palette — theme-adaptive chrome\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\"\n\n# Actuarial loss development triangle data\nnp.random.seed(42)\naccident_years = list(range(2015, 2025))\ndevelopment_periods = list(range(1, 11))\nn_years = len(accident_years)\nn_periods = len(development_periods)\n\n# 2019 is a catastrophe year with ~50% higher initial claims\nbase_claims = np.array([3200, 3450, 3100, 3600, 5200, 3500, 3900, 4100, 3700, 4200])\ndev_factors = np.array([2.50, 1.60, 1.30, 1.15, 1.08, 1.05, 1.03, 1.02, 1.01])\n\ncumulative = np.zeros((n_years, n_periods))\nfor i in range(n_years):\n    cumulative[i, 0] = base_claims[i] + np.random.normal(0, 100)\n    for j in range(1, n_periods):\n        noise = 1 + np.random.normal(0, 0.02)\n        cumulative[i, j] = cumulative[i, j - 1] * dev_factors[j - 1] * noise\ncumulative = np.round(cumulative, 0).astype(int)\n\nrows = []\nfor i, ay in enumerate(accident_years):\n    for j, dp in enumerate(development_periods):\n        is_projected = (i + j) >= n_years\n        rows.append(\n            {\n                \"accident_year\": ay,\n                \"development_period\": dp,\n                \"cumulative_amount\": cumulative[i, j],\n                \"is_projected\": is_projected,\n            }\n        )\n\ndf = pd.DataFrame(rows)\ndf[\"label\"] = df[\"cumulative_amount\"].apply(lambda v: f\"{v:,}\")\ndf[\"region\"] = df[\"is_projected\"].map({False: \"Actual\", True: \"Projected\"})\n\n# Theme-adaptive text color for cell annotations\namount_thresh = df[\"cumulative_amount\"].quantile(0.45)\nlight_ink = \"#FAF8F1\" if THEME == \"light\" else \"#F0EFE8\"\ndf[\"text_color\"] = df[\"cumulative_amount\"].apply(lambda v: light_ink if v > amount_thresh else INK)\n\n# Development factors row positioned below main triangle\ndev_factor_y = max(accident_years) + 1.3\ndf_dev = pd.DataFrame(\n    {\n        \"development_period\": [j + 1.5 for j in range(len(dev_factors))],\n        \"accident_year\": dev_factor_y,\n        \"label\": [f\"{f:.2f}\" for f in dev_factors],\n        \"cumulative_amount\": 0.0,\n        \"region\": \"Actual\",\n    }\n)\n\nplot = (\n    ggplot(df, aes(x=\"development_period\", y=\"accident_year\"))\n    + geom_tile(aes(fill=\"cumulative_amount\", alpha=\"region\"), color=PAGE_BG, size=0.8)\n    + geom_text(aes(label=\"label\", color=\"text_color\"), size=3.0)\n    + geom_text(\n        aes(x=\"development_period\", y=\"accident_year\", label=\"label\"),\n        data=df_dev,\n        size=3.0,\n        color=\"#009E73\",\n        fontweight=\"bold\",\n        inherit_aes=False,\n    )\n    # Imprint sequential: brand green (low magnitude) → blue (high magnitude)\n    + scale_fill_gradient(low=\"#009E73\", high=\"#4467A3\", name=\"Cumulative\\nAmount\")\n    + scale_alpha_manual(values={\"Actual\": 1.0, \"Projected\": 0.35}, name=\"Region\")\n    + scale_color_identity()\n    + scale_x_continuous(breaks=development_periods, labels=[str(d) for d in development_periods], expand=(0, 0.5))\n    + scale_y_reverse(\n        breaks=accident_years + [dev_factor_y],\n        labels=[str(y) for y in accident_years] + [\"Dev Factor\"],\n        expand=(0, 0.2, 0, 1.0),\n    )\n    + labs(\n        x=\"Development Period (Years)\",\n        y=\"Accident Year\",\n        title=\"heatmap-loss-triangle · python · plotnine · anyplot.ai\",\n        subtitle=\"Cumulative Paid Claims — Actual vs Projected  ·  Development Factors shown below\",\n    )\n    + theme_minimal()\n    + theme(\n        figure_size=(6, 6),\n        plot_title=element_text(size=12, ha=\"center\", weight=\"bold\", color=INK),\n        plot_subtitle=element_text(size=9, ha=\"center\", color=INK_SOFT),\n        plot_margin=0.02,\n        axis_title=element_text(size=10, color=INK),\n        axis_text_x=element_text(size=8, color=INK_SOFT),\n        axis_text_y=element_text(size=8, color=INK_SOFT),\n        legend_title=element_text(size=8, weight=\"bold\", color=INK),\n        legend_text=element_text(size=7, color=INK_SOFT),\n        legend_position=\"right\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_box_margin=0,\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=6, height=6, units=\"in\")\n"}