{"spec_id":"probability-weibull","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nprobability-weibull: Weibull Probability Plot for Reliability Analysis\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-06-07\n\"\"\"\n\n# ruff: noqa: F403, F405, E402\n\"\"\"anyplot.ai\nprobability-weibull: Weibull Probability Plot for Reliability Analysis\nLibrary: letsplot | Python 3.13\nQuality: pending | Created: 2026-06-07\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\nfrom lets_plot.export import ggsave\nfrom scipy import stats\n\n\nLetsPlot.setup_html()\n\n# Theme tokens (Imprint palette — see prompts/default-style-guide.md)\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 — categorical (theme-independent)\nBRAND = \"#009E73\"  # position 1 — Failure observations\nCOLOR_CENSORED = \"#C475FD\"  # position 2 — Censored observations\nCOLOR_ETA = \"#4467A3\"  # position 3 — characteristic life marker\n\n# Data — Bearing wear-out fatigue: shape=3.2 (steep wear-out mode), scale=8000 h\nnp.random.seed(42)\nshape_param = 3.2  # beta: wear-out failure mode (high shape → tight failure band)\nscale_param = 8000  # eta: characteristic life in hours\nn_failures = 25\nn_censored = 8\n\nfailure_times = np.sort(stats.weibull_min.rvs(shape_param, scale=scale_param, size=n_failures))\ncensored_times = np.sort(np.random.uniform(3000, 10000, n_censored))\n\nall_times = np.concatenate([failure_times, censored_times])\nis_failure = np.concatenate([np.ones(n_failures), np.zeros(n_censored)])\n\nsort_idx = np.argsort(all_times)\nall_times = all_times[sort_idx]\nis_failure = is_failure[sort_idx]\n\n# Median rank plotting positions (Bernard's approximation): (i - 0.3) / (n + 0.4)\nn_total = len(all_times)\nfailure_rank = np.cumsum(is_failure)\nmedian_ranks = np.clip((failure_rank - 0.3) / (n_total + 0.4), 1e-6, 1 - 1e-6)\n\n# Weibull linearization: y = ln(-ln(1 - F))\nweibull_y = np.log(-np.log(1 - median_ranks))\nlog_times = np.log(all_times)\n\ndf_all = pd.DataFrame(\n    {\"log_time\": log_times, \"weibull_y\": weibull_y, \"status\": np.where(is_failure == 1, \"Failure\", \"Censored\")}\n)\n\ndf_failures = df_all[df_all[\"status\"] == \"Failure\"].copy()\n\n# Linear fit on Weibull-linearized failure points: beta = slope, eta = exp(-intercept/slope)\nslope, intercept, r_value, _, _ = stats.linregress(df_failures[\"log_time\"], df_failures[\"weibull_y\"])\nbeta_fit = slope\neta_fit = np.exp(-intercept / slope)\n\n# Fitted regression line spanning data range with padding\nfit_x = np.linspace(np.log(all_times.min() * 0.6), np.log(all_times.max() * 1.4), 100)\nfit_y = slope * fit_x + intercept\ndf_fit = pd.DataFrame({\"log_time\": fit_x, \"weibull_y\": fit_y})\n\n# 63.2% characteristic life reference (where Weibull CDF = 1 - 1/e ≈ 0.632)\nref_y = np.log(-np.log(1 - 0.632))  # ≈ 0.0\nlog_eta = np.log(eta_fit)\n\n# Y-axis ticks — Weibull probability scale\nprob_levels = [0.01, 0.05, 0.10, 0.20, 0.50, 0.632, 0.90, 0.99]\ny_ticks = [np.log(-np.log(1 - p)) for p in prob_levels]\ny_labels = [\"1%\", \"5%\", \"10%\", \"20%\", \"50%\", \"63.2%\", \"90%\", \"99%\"]\n\n# X-axis ticks — hours on log scale\nx_vals = [1000, 2000, 4000, 6000, 8000, 12000]\nx_ticks = [np.log(v) for v in x_vals]\nx_labels = [\"1,000\", \"2,000\", \"4,000\", \"6,000\", \"8,000\", \"12,000\"]\n\n# Crosshair segments emphasising the characteristic life intersection\ndf_h_seg = pd.DataFrame({\"x\": [log_eta - 0.4], \"xend\": [log_eta + 0.4], \"y\": [ref_y], \"yend\": [ref_y]})\ndf_v_seg = pd.DataFrame({\"x\": [log_eta], \"xend\": [log_eta], \"y\": [ref_y - 0.28], \"yend\": [ref_y + 0.28]})\n\n# Characteristic life label — positioned above the crosshair to avoid data crowding\ndf_eta_label = pd.DataFrame({\"x\": [log_eta], \"y\": [ref_y + 0.55], \"label\": [f\"η = {eta_fit:.0f} hrs\\n(63.2%)\"]})\n\n# Parameter annotation — upper-left (high-probability region is sparse for wear-out data)\ndf_annot = pd.DataFrame(\n    {\n        \"x\": [x_ticks[0] + 0.1],\n        \"y\": [y_ticks[-2] - 0.1],\n        \"label\": [f\"β = {beta_fit:.2f}\\nη = {eta_fit:.0f} hrs\\nR² = {r_value**2:.4f}\"],\n    }\n)\n\nTITLE = \"probability-weibull · python · letsplot · anyplot.ai\"\n\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_border=element_blank(),\n    panel_grid_major=element_line(color=INK_MUTED, size=0.25),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    axis_line=element_line(color=INK_SOFT),\n    plot_title=element_text(color=INK, size=16),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_text(color=INK, size=10),\n    legend_position=[0.05, 0.17],\n)\n\n# Plot\nplot = (\n    ggplot(df_all, aes(x=\"log_time\", y=\"weibull_y\", color=\"status\", shape=\"status\"))\n    # Fitted regression line\n    + geom_line(\n        data=df_fit, mapping=aes(x=\"log_time\", y=\"weibull_y\"), color=INK, size=1.6, alpha=0.90, inherit_aes=False\n    )\n    # 63.2% horizontal reference\n    + geom_hline(yintercept=ref_y, linetype=\"dashed\", color=INK_MUTED, size=0.6)\n    # Vertical reference at eta\n    + geom_vline(xintercept=log_eta, linetype=\"dotted\", color=INK_MUTED, size=0.6)\n    # Crosshair emphasis at characteristic life intersection\n    + geom_segment(\n        data=df_h_seg,\n        mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"),\n        color=COLOR_ETA,\n        size=1.8,\n        alpha=0.8,\n        inherit_aes=False,\n    )\n    + geom_segment(\n        data=df_v_seg,\n        mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"),\n        color=COLOR_ETA,\n        size=1.8,\n        alpha=0.8,\n        inherit_aes=False,\n    )\n    # Failure / censored data points (color + shape redundancy for accessibility)\n    + geom_point(size=5, alpha=0.9, stroke=1.0)\n    # Diamond marker at characteristic life intersection\n    + geom_point(\n        data=pd.DataFrame({\"x\": [log_eta], \"y\": [ref_y]}),\n        mapping=aes(x=\"x\", y=\"y\"),\n        color=COLOR_ETA,\n        fill=COLOR_ETA,\n        size=8,\n        shape=18,\n        alpha=0.95,\n        inherit_aes=False,\n    )\n    # Characteristic life label above intersection to avoid crowding\n    + geom_text(\n        data=df_eta_label,\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\"),\n        size=5,\n        color=COLOR_ETA,\n        hjust=0.5,\n        fontface=\"bold\",\n        inherit_aes=False,\n    )\n    # Weibull parameter annotation (upper-left, away from dense data region)\n    + geom_text(\n        data=df_annot,\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\"),\n        size=6,\n        color=INK_SOFT,\n        hjust=0,\n        fontface=\"bold\",\n        inherit_aes=False,\n    )\n    + scale_color_manual(name=\"Observation\", values={\"Failure\": BRAND, \"Censored\": COLOR_CENSORED})\n    + scale_shape_manual(name=\"Observation\", values={\"Failure\": 16, \"Censored\": 1})\n    + scale_x_continuous(breaks=x_ticks, labels=x_labels)\n    + scale_y_continuous(breaks=y_ticks, labels=y_labels)\n    + labs(x=\"Time to Failure (hours)\", y=\"Cumulative Failure Probability\", title=TITLE)\n    + ggsize(800, 450)\n    + anyplot_theme\n)\n\n# Save\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}