{"spec_id":"curve-dose-response","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\ncurve-dose-response: Pharmacological Dose-Response Curve\nLibrary: plotnine 0.15.7 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_errorbar,\n    geom_hline,\n    geom_line,\n    geom_point,\n    geom_ribbon,\n    geom_segment,\n    geom_text,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_color_manual,\n    scale_fill_manual,\n    scale_x_log10,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\nfrom scipy.optimize import curve_fit\n\n\n# Theme tokens (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\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n_SUP = str.maketrans(\"0123456789\", \"⁰¹²³⁴⁵⁶⁷⁸⁹\")\n\n# Data\nnp.random.seed(42)\n\nconcentrations = np.logspace(-9, -4, 8)\ncompounds = [\"Erlotinib\", \"Gefitinib\"]\n\n\ndef logistic_4pl(x, bottom, top, ec50, hill):\n    return bottom + (top - bottom) / (1 + (ec50 / x) ** hill)\n\n\ntrue_params = {\n    \"Erlotinib\": {\"bottom\": 5, \"top\": 95, \"ec50\": 1e-7, \"hill\": 1.2},\n    \"Gefitinib\": {\"bottom\": 10, \"top\": 85, \"ec50\": 5e-7, \"hill\": 0.9},\n}\n\nrows = []\nfor conc in concentrations:\n    resp_a = logistic_4pl(conc, **true_params[\"Erlotinib\"]) + np.random.normal(0, 3)\n    resp_b = logistic_4pl(conc, **true_params[\"Gefitinib\"]) + np.random.normal(0, 3.5)\n    rows.append(\n        {\n            \"concentration\": conc,\n            \"response\": resp_a,\n            \"response_sem\": np.random.uniform(1.5, 4.0),\n            \"compound\": \"Erlotinib\",\n        }\n    )\n    rows.append(\n        {\n            \"concentration\": conc,\n            \"response\": resp_b,\n            \"response_sem\": np.random.uniform(2.0, 4.5),\n            \"compound\": \"Gefitinib\",\n        }\n    )\n\ndf = pd.DataFrame(rows)\n\n# Fit 4PL curves\nfit_params = {}\nfor compound in compounds:\n    subset = df[df[\"compound\"] == compound]\n    popt, pcov = curve_fit(\n        logistic_4pl, subset[\"concentration\"].values, subset[\"response\"].values, p0=[5, 90, 1e-6, 1.0], maxfev=10000\n    )\n    fit_params[compound] = {\"popt\": popt, \"pcov\": pcov}\n\n# Generate smooth fitted curves with 95% CI via delta method\nconc_smooth = np.logspace(-9.5, -3.5, 200)\nfit_rows = []\nfor compound, params in fit_params.items():\n    popt = params[\"popt\"]\n    pcov = params[\"pcov\"]\n    fitted = logistic_4pl(conc_smooth, *popt)\n    jacobian = np.zeros((len(conc_smooth), 4))\n    eps = 1e-8\n    for i in range(4):\n        popt_up, popt_dn = popt.copy(), popt.copy()\n        popt_up[i] += eps\n        popt_dn[i] -= eps\n        jacobian[:, i] = (logistic_4pl(conc_smooth, *popt_up) - logistic_4pl(conc_smooth, *popt_dn)) / (2 * eps)\n    se = np.sqrt(np.maximum(np.sum(jacobian @ pcov * jacobian, axis=1), 0))\n    for j, c in enumerate(conc_smooth):\n        fit_rows.append(\n            {\n                \"concentration\": c,\n                \"fitted\": fitted[j],\n                \"ci_lower\": fitted[j] - 1.96 * se[j],\n                \"ci_upper\": fitted[j] + 1.96 * se[j],\n                \"compound\": compound,\n            }\n        )\ndf_fit = pd.DataFrame(fit_rows)\n\n# EC50 reference data\ncolors = {\"Erlotinib\": IMPRINT_PALETTE[0], \"Gefitinib\": IMPRINT_PALETTE[1]}\nec50_rows = []\nfor compound, params in fit_params.items():\n    bottom, top, ec50, hill = params[\"popt\"]\n    ec50_rows.append(\n        {\n            \"compound\": compound,\n            \"ec50\": ec50,\n            \"half_response\": bottom + (top - bottom) / 2,\n            \"bottom\": bottom,\n            \"top\": top,\n            \"x_start\": 1e-10,\n        }\n    )\ndf_ec50 = pd.DataFrame(ec50_rows)\n\n# EC50 annotation labels — staggered to prevent crowding\nec50_labels = []\nfor i, (_, row) in enumerate(df_ec50.iterrows()):\n    ec50_val = row[\"ec50\"]\n    exp = int(np.floor(np.log10(ec50_val)))\n    mantissa = ec50_val / 10**exp\n    exp_str = str(abs(exp)).translate(_SUP)\n    label = f\"EC₅₀={mantissa:.1f}×10⁻{exp_str} M\"\n    # Erlotinib: left of EC50, above midpoint; Gefitinib: right of EC50, below midpoint\n    x_pos = ec50_val * 0.12 if i == 0 else ec50_val * 6\n    y_pos = row[\"half_response\"] + 10 if i == 0 else row[\"half_response\"] - 12\n    ec50_labels.append({\"concentration\": x_pos, \"response\": y_pos, \"label\": label, \"compound\": row[\"compound\"]})\ndf_ec50_labels = pd.DataFrame(ec50_labels)\n\n# Plot\nplot = (\n    ggplot()\n    + geom_ribbon(aes(x=\"concentration\", ymin=\"ci_lower\", ymax=\"ci_upper\", fill=\"compound\"), data=df_fit, alpha=0.18)\n    + geom_line(aes(x=\"concentration\", y=\"fitted\", color=\"compound\"), data=df_fit, size=1.0)\n    + geom_errorbar(\n        aes(x=\"concentration\", ymin=\"response - response_sem\", ymax=\"response + response_sem\", color=\"compound\"),\n        data=df,\n        width=0.08,\n        size=0.4,\n    )\n    + geom_point(aes(x=\"concentration\", y=\"response\", color=\"compound\"), data=df, size=2.5, fill=\"white\", stroke=0.8)\n)\n\n# EC50 reference lines\nfor _, row in df_ec50.iterrows():\n    col = colors[row[\"compound\"]]\n    rd = pd.DataFrame([row])\n    plot = (\n        plot\n        + geom_segment(\n            aes(x=\"ec50\", xend=\"ec50\", y=0, yend=\"half_response\"),\n            data=rd,\n            linetype=\"dashed\",\n            color=col,\n            size=0.5,\n            alpha=0.5,\n        )\n        + geom_segment(\n            aes(x=\"x_start\", xend=\"ec50\", y=\"half_response\", yend=\"half_response\"),\n            data=rd,\n            linetype=\"dashed\",\n            color=col,\n            size=0.5,\n            alpha=0.5,\n        )\n    )\n\n# EC50 value annotations\nplot = plot + geom_text(\n    aes(x=\"concentration\", y=\"response\", label=\"label\", color=\"compound\"),\n    data=df_ec50_labels,\n    size=3.0,\n    ha=\"left\",\n    fontweight=\"bold\",\n)\n\n# Top/bottom asymptote reference lines\nfor _, row in df_ec50.iterrows():\n    col = colors[row[\"compound\"]]\n    plot = (\n        plot\n        + geom_hline(yintercept=row[\"top\"], linetype=\"dotted\", color=col, size=0.4, alpha=0.3)\n        + geom_hline(yintercept=row[\"bottom\"], linetype=\"dotted\", color=col, size=0.4, alpha=0.3)\n    )\n\n# Scales and style\nplot = (\n    plot\n    + scale_x_log10(\n        labels=lambda vals: [f\"10⁻{str(abs(int(round(np.log10(v))))).translate(_SUP)}\" if v > 0 else \"\" for v in vals]\n    )\n    + scale_y_continuous(breaks=range(0, 101, 20), limits=(-5, 110))\n    + scale_color_manual(values=colors)\n    + scale_fill_manual(values=colors)\n    + labs(\n        x=\"Concentration (M)\",\n        y=\"Response (%)\",\n        title=\"curve-dose-response · python · plotnine · anyplot.ai\",\n        color=\"Compound\",\n        fill=\"Compound\",\n    )\n    + guides(color=guide_legend(override_aes={\"size\": 3}), fill=guide_legend(title=\"Compound\"))\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        text=element_text(size=7, color=INK),\n        axis_title=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        plot_title=element_text(size=12, color=INK),\n        legend_title=element_text(size=9, color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_position=(0.82, 0.22),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_key_size=12,\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_y=element_line(color=INK, size=0.2, alpha=0.15),\n        axis_line=element_line(color=INK_SOFT, size=0.4),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}