{"spec_id":"probability-weibull","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nprobability-weibull: Weibull Probability Plot for Reliability Analysis\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-07\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove the script's own directory from sys.path so 'import matplotlib' resolves\n# to the installed package rather than this file (naming-conflict guard).\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _script_dir]\n\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as ticker\nimport numpy as np\nfrom matplotlib.patheffects import withStroke\nfrom scipy import stats\n\n\n# Theme-adaptive chrome — Imprint palette\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# Imprint palette — first series brand green; semantic red for threshold line\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nBRAND = IMPRINT_PALETTE[0]  # \"#009E73\" — failures + fit (first categorical series)\nRED = IMPRINT_PALETTE[4]  # \"#AE3030\" — 63.2% reference (semantic alarm threshold)\n\n# Data — turbine blade fatigue-life (hours)\nnp.random.seed(42)\nshape_true, scale_true = 2.5, 8000\nn_total = 30\nn_failures = 24\nn_censored = n_total - n_failures\n\nfailure_times = np.sort(stats.weibull_min.rvs(shape_true, scale=scale_true, size=n_failures))\nfailure_times[2] *= 0.75  # early outlier — adds diagnostic interest\nfailure_times[-3] *= 1.25  # late outlier\ncensored_times = np.sort(np.random.uniform(2000, 10000, size=n_censored))\n\nall_times = np.concatenate([failure_times, censored_times])\nis_censored = np.concatenate([np.zeros(n_failures, dtype=bool), np.ones(n_censored, dtype=bool)])\nsort_idx = np.argsort(all_times)\nall_times = all_times[sort_idx]\nis_censored = is_censored[sort_idx]\n\n# Median rank plotting positions (Benard's approximation)\nfailure_indices = np.where(~is_censored)[0]\nfailure_times_sorted = all_times[failure_indices]\nranks = np.arange(1, len(failure_times_sorted) + 1)\nmedian_rank = (ranks - 0.3) / (len(failure_times_sorted) + 0.4)\n\n# Weibull linearized transform: ln(-ln(1-F))\nweibull_y = np.log(-np.log(1 - median_rank))\n\n# Least-squares fit on log(time) vs Weibull_y\nlog_times = np.log(failure_times_sorted)\nslope, intercept = np.polyfit(log_times, weibull_y, 1)\nbeta = slope\neta = np.exp(-intercept / beta)\n\n# Censored plotting positions (interpolated from adjacent median ranks)\ncensored_indices = np.where(is_censored)[0]\ncensored_times_vals = all_times[censored_indices]\ncensored_y_positions = []\nfor ct in censored_times_vals:\n    idx = np.searchsorted(failure_times_sorted, ct, side=\"right\")\n    if idx == 0:\n        f_val = 0.05\n    elif idx >= len(median_rank):\n        f_val = median_rank[-1]\n    else:\n        f_val = median_rank[idx - 1]\n    censored_y_positions.append(np.log(-np.log(1 - min(f_val, 0.99))))\ncensored_y_positions = np.array(censored_y_positions)\n\n# Fit line coordinates\nfit_x = np.linspace(np.min(failure_times_sorted) * 0.5, np.max(failure_times_sorted) * 1.5, 200)\nfit_y = beta * np.log(fit_x) - beta * np.log(eta)\n\n# 63.2% reference — characteristic life (where Weibull CDF = 1 - 1/e)\ny_632 = np.log(-np.log(1 - 0.632))\n\n# Explicit y range for reliable twinx sync\nall_y = np.concatenate([weibull_y, censored_y_positions, fit_y])\ny_pad = 0.35\ny_min = float(all_y.min()) - y_pad\ny_max = float(all_y.max()) + y_pad\n\n# Canvas — 3200×1800 px (landscape 16:9)\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Fit line with PathEffects halo — separates it visually from dense marker clusters\nax.plot(\n    fit_x,\n    fit_y,\n    color=BRAND,\n    linewidth=2.5,\n    zorder=2,\n    label=\"Weibull fit\",\n    path_effects=[withStroke(linewidth=5, foreground=PAGE_BG)],\n)\n\n# Failure markers (filled)\nax.scatter(\n    failure_times_sorted, weibull_y, s=80, color=BRAND, edgecolors=PAGE_BG, linewidth=0.8, zorder=3, label=\"Failures\"\n)\n\n# Censored markers (hollow — visually distinct from failures per spec)\nax.scatter(\n    censored_times_vals,\n    censored_y_positions,\n    s=80,\n    facecolors=\"none\",\n    edgecolors=BRAND,\n    linewidth=1.5,\n    zorder=3,\n    label=\"Censored\",\n)\n\n# 63.2% reference line (semantic red = alarm / design threshold)\nax.axhline(y=y_632, color=RED, linewidth=1.5, linestyle=\"--\", alpha=0.85, zorder=1, label=\"63.2% (characteristic life)\")\n\n# Weibull parameter annotation\nax.text(\n    0.97,\n    0.08,\n    f\"β = {beta:.2f}  (shape)\\nη = {eta:.0f} h  (scale)\",\n    transform=ax.transAxes,\n    fontsize=8,\n    ha=\"right\",\n    va=\"bottom\",\n    color=INK,\n    bbox={\n        \"boxstyle\": \"round,pad=0.4\",\n        \"facecolor\": ELEVATED_BG,\n        \"edgecolor\": INK_SOFT,\n        \"alpha\": 0.95,\n        \"linewidth\": 0.8,\n    },\n)\n\n# Set primary y-axis limits before twinx\nax.set_ylim(y_min, y_max)\n\n# Secondary y-axis — cumulative probability labels (bridges raw Weibull scale)\nprob_levels = [0.01, 0.05, 0.10, 0.20, 0.50, 0.632, 0.90, 0.99]\nprob_y_ticks = [np.log(-np.log(1 - p)) for p in prob_levels]\nax2 = ax.twinx()\nax2.set_ylim(y_min, y_max)\nax2.set_yticks(prob_y_ticks)\nax2.set_yticklabels([f\"{p * 100:.1f}%\" for p in prob_levels])\nax2.tick_params(axis=\"y\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nax2.spines[\"top\"].set_visible(False)\nax2.spines[\"left\"].set_visible(False)\nax2.spines[\"bottom\"].set_visible(False)\nax2.spines[\"right\"].set_color(INK_SOFT)\nax2.spines[\"right\"].set_linewidth(0.6)\nax2.set_ylabel(\"Cumulative Probability\", fontsize=10, color=INK_SOFT, labelpad=6)\n\n# Title — length-adapted fontsize to avoid overflow\ntitle = \"probability-weibull · python · matplotlib · anyplot.ai\"\ntitle_fontsize = max(8, round(12 * 67 / len(title))) if len(title) > 67 else 12\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK, pad=8)\n\n# Primary axes labels and ticks\nax.set_xscale(\"log\")\nax.set_xlabel(\"Time to Failure (hours)\", fontsize=10, color=INK, labelpad=4)\nax.set_ylabel(\"ln(−ln(1−F))\", fontsize=10, color=INK, labelpad=4)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nax.xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f\"{x:,.0f}\"))\n\n# Spines — L-shaped frame\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nfor spine in (\"bottom\", \"left\"):\n    ax.spines[spine].set_color(INK_SOFT)\n    ax.spines[spine].set_linewidth(0.6)\n\n# Grid — subtle, y-axis heavier than x\nax.yaxis.grid(True, alpha=0.15, linewidth=0.6, color=INK)\nax.xaxis.grid(True, alpha=0.08, linewidth=0.4, color=INK)\nax.set_axisbelow(True)\n\n# Legend\nleg = ax.legend(fontsize=8, loc=\"upper left\", framealpha=0.95)\nif leg:\n    leg.get_frame().set_facecolor(ELEVATED_BG)\n    leg.get_frame().set_edgecolor(INK_SOFT)\n    leg.get_frame().set_linewidth(0.8)\n    plt.setp(leg.get_texts(), color=INK_SOFT)\n\n# Margins — leave room for right y-axis label; no bbox_inches='tight' on savefig\nfig.subplots_adjust(left=0.09, right=0.85, top=0.91, bottom=0.12)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}