{"spec_id":"curve-oc","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\ncurve-oc: Operating Characteristic (OC) Curve\nLibrary: plotly 6.8.0 | Python 3.13.14\nQuality: 92/100 | Updated: 2026-06-20\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent self-import: remove this script's directory from sys.path so that\n# \"import plotly\" resolves to the installed package, not this file.\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _here]\n\nimport numpy as np\nimport plotly.graph_objects as go\nfrom scipy.stats import binom\n\n\n# Theme\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\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\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint categorical palette\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data\nfraction_defective = np.linspace(0, 0.20, 200)\n\nsampling_plans = [\n    {\"n\": 50, \"c\": 1, \"label\": \"n=50, c=1\", \"dash\": \"solid\"},\n    {\"n\": 100, \"c\": 2, \"label\": \"n=100, c=2\", \"dash\": \"dash\"},\n    {\"n\": 150, \"c\": 3, \"label\": \"n=150, c=3\", \"dash\": \"dot\"},\n]\n\ncolors = [IMPRINT_PALETTE[0], IMPRINT_PALETTE[1], IMPRINT_PALETTE[2]]  # green, lavender, blue\n\naql = 0.02\nltpd = 0.08\n\n# Compute acceptance probabilities\noc_curves = {}\nfor plan in sampling_plans:\n    prob_accept = binom.cdf(plan[\"c\"], plan[\"n\"], fraction_defective)\n    oc_curves[plan[\"label\"]] = prob_accept\n\n# Plot\nfig = go.Figure()\n\n# OC curves with distinct line styles for redundant encoding\nfor i, plan in enumerate(sampling_plans):\n    label = plan[\"label\"]\n    fig.add_trace(\n        go.Scatter(\n            x=fraction_defective,\n            y=oc_curves[label],\n            mode=\"lines\",\n            name=label,\n            line={\"color\": colors[i], \"width\": 3, \"dash\": plan[\"dash\"]},\n            hovertemplate=(f\"<b>{label}</b><br>Fraction defective: %{{x:.3f}}<br>P(accept): %{{y:.3f}}<extra></extra>\"),\n        )\n    )\n\n# Risk calculations on the first plan\nalpha_plan = sampling_plans[0]\nprob_accept_aql = binom.cdf(alpha_plan[\"c\"], alpha_plan[\"n\"], aql)\nalpha = 1 - prob_accept_aql\n\nbeta = binom.cdf(alpha_plan[\"c\"], alpha_plan[\"n\"], ltpd)\n\n# Producer's risk shading (area between P(accept) and 1.0 near AQL)\np_region_x = fraction_defective[fraction_defective <= aql]\np_region_oc = binom.cdf(alpha_plan[\"c\"], alpha_plan[\"n\"], p_region_x)\nfig.add_trace(\n    go.Scatter(\n        x=np.concatenate([p_region_x, p_region_x[::-1]]),\n        y=np.concatenate([p_region_oc, np.ones(len(p_region_x))]),\n        fill=\"toself\",\n        fillcolor=\"rgba(0,158,115,0.15)\",\n        line={\"width\": 0},\n        name=f\"Producer's risk α = {alpha:.3f}\",\n        hoverinfo=\"skip\",\n        showlegend=True,\n    )\n)\n\n# Consumer's risk shading (area from 0 to OC curve beyond LTPD)\nc_region_x = fraction_defective[fraction_defective >= ltpd]\nc_region_oc = binom.cdf(alpha_plan[\"c\"], alpha_plan[\"n\"], c_region_x)\nfig.add_trace(\n    go.Scatter(\n        x=np.concatenate([c_region_x, c_region_x[::-1]]),\n        y=np.concatenate([np.zeros(len(c_region_x)), c_region_oc[::-1]]),\n        fill=\"toself\",\n        fillcolor=\"rgba(174,48,48,0.15)\",\n        line={\"width\": 0},\n        name=f\"Consumer's risk β = {beta:.3f}\",\n        hoverinfo=\"skip\",\n        showlegend=True,\n    )\n)\n\n# Producer's risk diamond marker at AQL\nfig.add_trace(\n    go.Scatter(\n        x=[aql],\n        y=[prob_accept_aql],\n        mode=\"markers+text\",\n        marker={\"size\": 14, \"color\": colors[0], \"symbol\": \"diamond\", \"line\": {\"color\": PAGE_BG, \"width\": 2}},\n        text=[f\"α={alpha:.3f}\"],\n        textposition=\"bottom right\",\n        textfont={\"size\": 10, \"color\": colors[0]},\n        showlegend=False,\n        hovertemplate=(\n            f\"<b>Producer's Risk (α)</b><br>\"\n            f\"At AQL = {aql}<br>\"\n            f\"P(accept) = {prob_accept_aql:.3f}<br>\"\n            f\"α = {alpha:.3f}<extra></extra>\"\n        ),\n    )\n)\n\n# Consumer's risk diamond marker at LTPD — matte red for semantic \"bad lot accepted\"\nfig.add_trace(\n    go.Scatter(\n        x=[ltpd],\n        y=[beta],\n        mode=\"markers+text\",\n        marker={\"size\": 14, \"color\": IMPRINT_PALETTE[4], \"symbol\": \"diamond\", \"line\": {\"color\": PAGE_BG, \"width\": 2}},\n        text=[f\"β={beta:.3f}\"],\n        textposition=\"top right\",\n        textfont={\"size\": 10, \"color\": IMPRINT_PALETTE[4]},\n        showlegend=False,\n        hovertemplate=(f\"<b>Consumer's Risk (β)</b><br>At LTPD = {ltpd}<br>P(accept) = {beta:.3f}<extra></extra>\"),\n    )\n)\n\n# AQL reference line\nfig.add_shape(type=\"line\", x0=aql, x1=aql, y0=0, y1=1, line={\"color\": INK_MUTED, \"width\": 1.5, \"dash\": \"dash\"})\nfig.add_annotation(\n    x=aql, y=1.05, text=f\"<b>AQL={aql}</b>\", showarrow=False, font={\"size\": 10, \"color\": INK_SOFT}, yref=\"y\"\n)\n\n# LTPD reference line\nfig.add_shape(type=\"line\", x0=ltpd, x1=ltpd, y0=0, y1=1, line={\"color\": INK_MUTED, \"width\": 1.5, \"dash\": \"dash\"})\nfig.add_annotation(\n    x=ltpd, y=1.05, text=f\"<b>LTPD={ltpd}</b>\", showarrow=False, font={\"size\": 10, \"color\": INK_SOFT}, yref=\"y\"\n)\n\n# Layout\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": \"curve-oc · python · plotly · anyplot.ai\",\n        \"font\": {\"size\": 16, \"color\": INK, \"family\": \"Arial, Helvetica, sans-serif\"},\n        \"x\": 0.5,\n        \"y\": 0.97,\n        \"xanchor\": \"center\",\n    },\n    xaxis={\n        \"title\": {\"text\": \"Fraction Defective (p)\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": False,\n        \"showline\": True,\n        \"linewidth\": 1,\n        \"linecolor\": INK_SOFT,\n        \"range\": [0, 0.20],\n        \"dtick\": 0.02,\n        \"tickformat\": \".2f\",\n        \"spikemode\": \"across\",\n        \"spikethickness\": 1,\n        \"spikecolor\": INK_MUTED,\n        \"spikedash\": \"dot\",\n    },\n    yaxis={\n        \"title\": {\"text\": \"Probability of Acceptance\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": True,\n        \"gridcolor\": GRID,\n        \"gridwidth\": 0.5,\n        \"range\": [-0.02, 1.12],\n        \"zeroline\": False,\n        \"showline\": True,\n        \"linewidth\": 1,\n        \"linecolor\": INK_SOFT,\n        \"dtick\": 0.2,\n        \"spikemode\": \"across\",\n        \"spikethickness\": 1,\n        \"spikecolor\": INK_MUTED,\n        \"spikedash\": \"dot\",\n    },\n    legend={\n        \"title\": {\"text\": \"Sampling Plans\", \"font\": {\"size\": 10, \"color\": INK_SOFT}},\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"x\": 0.99,\n        \"y\": 0.98,\n        \"xanchor\": \"right\",\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n    },\n    hovermode=\"x unified\",\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    margin={\"l\": 80, \"r\": 40, \"t\": 80, \"b\": 60},\n    width=800,\n    height=450,\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}