{"spec_id":"bland-altman-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nbland-altman-basic: Bland-Altman Agreement Plot\nLibrary: letsplot 4.11.0 | Python 3.13.14\nQuality: 92/100 | Updated: 2026-08-11\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_hline,\n    geom_label,\n    geom_point,\n    geom_rect,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    layer_tooltips,\n    theme,\n    theme_minimal,\n)\n\n\nLetsPlot.setup_html()\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\"\nGRID_COLOR = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint palette\nBRAND = \"#009E73\"  # Position 1 - first categorical series\nSECONDARY = \"#C475FD\"  # Position 2 - limits of agreement\n\n# Data: Simulated blood pressure readings from two sphygmomanometers\nnp.random.seed(42)\nn = 80\n\n# True systolic BP values (realistic range: 100-160 mmHg)\ntrue_bp = np.random.normal(125, 15, n)\n\n# Method 1: Reference standard (small measurement error)\nmethod1 = true_bp + np.random.normal(0, 3, n)\n\n# Method 2: New device (slight positive bias + slightly larger error)\nmethod2 = true_bp + np.random.normal(2, 4, n)\n\n# Bland-Altman calculations\nmean_values = (method1 + method2) / 2\ndiff_values = method1 - method2\n\nmean_diff = np.mean(diff_values)\nstd_diff = np.std(diff_values, ddof=1)\nupper_loa = mean_diff + 1.96 * std_diff\nlower_loa = mean_diff - 1.96 * std_diff\n\n# Point-level data - also drives the interactive hover tooltips\ndf = pd.DataFrame({\"method1\": method1, \"method2\": method2, \"mean\": mean_values, \"diff\": diff_values})\n\n# Agreement band: shaded rectangle spanning the limits of agreement\nx_pad = (df[\"mean\"].max() - df[\"mean\"].min()) * 0.05\nband_df = pd.DataFrame(\n    {\"xmin\": [df[\"mean\"].min() - x_pad], \"xmax\": [df[\"mean\"].max() + x_pad], \"ymin\": [lower_loa], \"ymax\": [upper_loa]}\n)\n\n# Annotation labels, right-aligned with extra headroom past the data range so the\n# label never sits on top of whichever point happens to be rightmost\nannot_x = df[\"mean\"].max() + x_pad * 3\ny_offset = 1.0\nannot_df = pd.DataFrame(\n    {\n        \"x\": [annot_x, annot_x, annot_x],\n        \"y\": [mean_diff + y_offset, upper_loa + y_offset, lower_loa - y_offset],\n        \"label\": [\n            f\"Mean bias: {mean_diff:.2f} mmHg\",\n            f\"+1.96 SD: {upper_loa:.2f} mmHg\",\n            f\"-1.96 SD: {lower_loa:.2f} mmHg\",\n        ],\n        \"line_type\": [\"bias\", \"loa\", \"loa\"],\n    }\n)\n\n# Build plot with theme-adaptive styling\nplot = (\n    ggplot()\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"), data=band_df, fill=SECONDARY, alpha=0.08, size=0\n    )\n    + geom_point(\n        aes(x=\"mean\", y=\"diff\"),\n        data=df,\n        color=BRAND,\n        size=2.5,\n        alpha=0.7,\n        stroke=0.4,\n        tooltips=layer_tooltips()\n        .line(\"Method 1|@method1\")\n        .line(\"Method 2|@method2\")\n        .line(\"Mean|@mean\")\n        .line(\"Difference|@diff\"),\n    )\n    + geom_hline(yintercept=mean_diff, color=BRAND, size=1.2)\n    + geom_hline(yintercept=upper_loa, color=SECONDARY, size=0.7, linetype=\"dashed\")\n    + geom_hline(yintercept=lower_loa, color=SECONDARY, size=0.7, linetype=\"dashed\")\n    + geom_label(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=annot_df[annot_df[\"line_type\"] == \"bias\"],\n        size=4,\n        color=BRAND,\n        fill=ELEVATED_BG,\n        hjust=1,\n        label_padding=0.3,\n        label_r=0.1,\n    )\n    + geom_label(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=annot_df[annot_df[\"line_type\"] == \"loa\"],\n        size=4,\n        color=SECONDARY,\n        fill=ELEVATED_BG,\n        hjust=1,\n        label_padding=0.3,\n        label_r=0.1,\n    )\n    + labs(\n        x=\"Mean of Two Methods (mmHg)\",\n        y=\"Difference (Method 1 - Method 2) (mmHg)\",\n        title=\"bland-altman-basic · python · letsplot · anyplot.ai\",\n    )\n    + ggsize(800, 450)\n    + theme_minimal()\n    + theme(\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=GRID_COLOR, size=0.3, linetype=\"solid\"),\n        panel_grid_minor=element_blank(),\n        plot_title=element_text(size=16, color=INK),\n        axis_title=element_text(size=12, color=INK),\n        axis_text=element_text(size=10, color=INK_SOFT),\n        axis_line=element_line(color=INK_SOFT, size=0.5),\n    )\n)\n\n# Save PNG and HTML with theme-suffixed filenames\nggsave(plot, f\"plot-{THEME}.png\", scale=4, path=\".\")\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}