{"spec_id":"probability-weibull","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nprobability-weibull: Weibull Probability Plot for Reliability Analysis\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-07\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom scipy import stats\n\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — first series always #009E73\nCLR_FAILURE = \"#009E73\"  # brand green — primary data (failures)\nCLR_CENSORED = \"#DDCC77\"  # amber — suspended/uncertain observations\n\n# Data - Turbine blade fatigue-life (hours)\nnp.random.seed(42)\nn_failures = 25\nn_censored = 5\nshape_true = 2.5\nscale_true = 5000\n\nfailure_times = np.sort(stats.weibull_min.rvs(shape_true, scale=scale_true, size=n_failures))\ncensored_times = np.sort(np.random.uniform(1000, 4500, size=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 for failures only\nfailure_ranks = np.cumsum(is_failure)\nn_total = n_failures + n_censored\nmedian_rank = (failure_ranks - 0.3) / (n_total + 0.4)\n\n# Weibull y-axis transform: ln(-ln(1 - F))\nweibull_y = np.log(-np.log(1 - median_rank))\nlog_times = np.log(all_times)\n\nfailure_mask = is_failure == 1\nslope, intercept, _, _, _ = stats.linregress(log_times[failure_mask], weibull_y[failure_mask])\nbeta_est = slope\neta_est = np.exp(-intercept / slope)\n\ndf = pd.DataFrame(\n    {\n        \"time\": all_times,\n        \"log_time\": log_times,\n        \"weibull_y\": weibull_y,\n        \"status\": np.where(is_failure == 1, \"Failure\", \"Censored\"),\n    }\n)\n\n# Fitted line data\nfit_log_x = np.linspace(np.log(all_times.min() * 0.7), np.log(all_times.max() * 1.3), 200)\nfit_y = slope * fit_log_x + intercept\nfit_time = np.exp(fit_log_x)\ndf_fit = pd.DataFrame({\"time\": fit_time, \"weibull_y\": fit_y})\n\n# Reference line at 63.2% (characteristic life)\nref_y = np.log(-np.log(1 - 0.632))\n\n# Y-axis range\ndata_y_min = weibull_y[failure_mask].min()\ndata_y_max = weibull_y[failure_mask].max()\ny_padding = (data_y_max - data_y_min) * 0.2\ny_min = data_y_min - y_padding\ny_max = data_y_max + y_padding\n\n# X-axis domain\nx_min = all_times.min() * 0.7\nx_max = all_times.max() * 1.3\n\n# Weibull probability labels for y-axis\nprob_levels = np.array([0.01, 0.05, 0.10, 0.20, 0.50, 0.632, 0.90, 0.95, 0.99])\nweibull_ticks = np.log(-np.log(1 - prob_levels))\nprob_labels = [\"1%\", \"5%\", \"10%\", \"20%\", \"50%\", \"63.2%\", \"90%\", \"95%\", \"99%\"]\nmask_ticks = (weibull_ticks >= y_min) & (weibull_ticks <= y_max)\nvisible_ticks = weibull_ticks[mask_ticks]\nvisible_labels = [prob_labels[i] for i in range(len(prob_labels)) if mask_ticks[i]]\n\nlabel_cases = \" : \".join(\n    f\"abs(datum.value - {val:.4f}) < 0.01 ? '{lbl}'\" for val, lbl in zip(visible_ticks, visible_labels, strict=True)\n)\ny_label_expr = f\"{label_cases} : ''\"\n\n# Shared axis encodings\nx_enc = alt.X(\n    \"time:Q\",\n    scale=alt.Scale(type=\"log\", domain=[x_min, x_max], nice=False),\n    title=\"Time to Failure (hours)\",\n    axis=alt.Axis(format=\"~s\"),\n)\ny_enc = alt.Y(\n    \"weibull_y:Q\",\n    scale=alt.Scale(domain=[y_min, y_max]),\n    title=\"Cumulative Failure Probability\",\n    axis=alt.Axis(values=visible_ticks.tolist(), labelExpr=y_label_expr),\n)\ntooltip_enc = [\n    alt.Tooltip(\"time:Q\", title=\"Time (hrs)\", format=\",.0f\"),\n    alt.Tooltip(\"status:N\", title=\"Status\"),\n    alt.Tooltip(\"weibull_y:Q\", title=\"Weibull Y\", format=\".2f\"),\n]\n\n# Failure points (filled circles)\nfailures_chart = (\n    alt.Chart(df[df[\"status\"] == \"Failure\"])\n    .mark_point(size=110, filled=True, color=CLR_FAILURE, strokeWidth=1.5, stroke=PAGE_BG)\n    .encode(x=x_enc, y=y_enc, tooltip=tooltip_enc)\n)\n\n# Censored points (open triangles)\ncensored_chart = (\n    alt.Chart(df[df[\"status\"] == \"Censored\"])\n    .mark_point(size=80, filled=False, shape=\"triangle-up\", color=CLR_CENSORED, strokeWidth=2.0)\n    .encode(x=x_enc, y=y_enc, tooltip=tooltip_enc)\n)\n\n# Legend layer — invisible markers that expose color+shape in the legend\nlegend_points = (\n    alt.Chart(df)\n    .mark_point(size=80, strokeWidth=2)\n    .encode(\n        x=alt.X(\"time:Q\"),\n        y=alt.Y(\"weibull_y:Q\"),\n        color=alt.Color(\n            \"status:N\",\n            scale=alt.Scale(domain=[\"Failure\", \"Censored\"], range=[CLR_FAILURE, CLR_CENSORED]),\n            legend=alt.Legend(title=\"Observation Type\", symbolSize=80, labelFontSize=10, titleFontSize=10),\n        ),\n        shape=alt.Shape(\n            \"status:N\", scale=alt.Scale(domain=[\"Failure\", \"Censored\"], range=[\"circle\", \"triangle-up\"]), legend=None\n        ),\n        opacity=alt.value(0),\n    )\n)\n\n# Fitted Weibull line\nfit_line = (\n    alt.Chart(df_fit)\n    .mark_line(strokeWidth=2.5, color=INK_SOFT, strokeDash=[8, 4])\n    .encode(x=alt.X(\"time:Q\", scale=alt.Scale(type=\"log\")), y=alt.Y(\"weibull_y:Q\"))\n)\n\n# Reference line at 63.2% characteristic life\ndf_ref = pd.DataFrame({\"weibull_y\": [ref_y, ref_y], \"time\": [x_min, x_max]})\nref_line = (\n    alt.Chart(df_ref)\n    .mark_line(strokeWidth=1.5, color=INK_MUTED, strokeDash=[4, 4])\n    .encode(x=alt.X(\"time:Q\", scale=alt.Scale(type=\"log\")), y=alt.Y(\"weibull_y:Q\"))\n)\n\n# Parameter annotation (beta and eta in lower-right)\ndf_annotation = pd.DataFrame(\n    {\n        \"time\": [all_times.max() * 0.85],\n        \"weibull_y\": [y_min + (y_max - y_min) * 0.10],\n        \"text\": [f\"β = {beta_est:.2f}   η = {eta_est:.0f} hrs\"],\n    }\n)\nparam_text = (\n    alt.Chart(df_annotation)\n    .mark_text(fontSize=12, align=\"right\", fontWeight=\"bold\", color=INK)\n    .encode(x=alt.X(\"time:Q\", scale=alt.Scale(type=\"log\")), y=alt.Y(\"weibull_y:Q\"), text=\"text:N\")\n)\n\n# 63.2% label\ndf_ref_label = pd.DataFrame(\n    {\"time\": [x_min * 1.05], \"weibull_y\": [ref_y + 0.12], \"text\": [\"63.2% Characteristic Life\"]}\n)\nref_label = (\n    alt.Chart(df_ref_label)\n    .mark_text(fontSize=10, align=\"left\", color=INK_MUTED, fontStyle=\"italic\")\n    .encode(x=alt.X(\"time:Q\", scale=alt.Scale(type=\"log\")), y=alt.Y(\"weibull_y:Q\"), text=\"text:N\")\n)\n\n# Interactive hover highlight on failure points\nhighlight = alt.selection_point(name=\"hover\", on=\"pointerover\", fields=[\"status\"], empty=False)\nfailures_interactive = failures_chart.add_params(highlight).encode(\n    size=alt.condition(highlight, alt.value(170), alt.value(110))\n)\n\n# Title: 50 chars — under 67 baseline, no fontsize scaling needed\ntitle_str = \"probability-weibull · python · altair · anyplot.ai\"\n\n# Combine all layers\nchart = (\n    (ref_line + fit_line + failures_interactive + censored_chart + legend_points + param_text + ref_label)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            title_str,\n            fontSize=16,\n            fontWeight=\"bold\",\n            color=INK,\n            subtitle=\"Turbine Blade Fatigue-Life Analysis  —  Weibull Distribution Fit\",\n            subtitleFontSize=12,\n            subtitleColor=INK_SOFT,\n            subtitlePadding=6,\n            anchor=\"start\",\n            offset=12,\n        ),\n    )\n    .configure_view(continuousWidth=620, continuousHeight=320, strokeWidth=0, fill=PAGE_BG)\n    .configure_axisX(\n        labelFontSize=10,\n        titleFontSize=12,\n        gridOpacity=0.08,\n        gridDash=[2, 4],\n        gridColor=INK,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        titlePadding=10,\n    )\n    .configure_axisY(\n        labelFontSize=10,\n        titleFontSize=12,\n        gridOpacity=0.15,\n        gridDash=[3, 3],\n        gridColor=INK,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        titlePadding=10,\n    )\n    .configure_legend(\n        orient=\"top-left\",\n        padding=12,\n        cornerRadius=6,\n        strokeColor=INK_SOFT,\n        fillColor=ELEVATED_BG,\n        labelFontSize=10,\n        titleFontSize=10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n)\n\n# Save PNG (scale_factor=4.0 → inner view 620×320 maps to ~3200×1800 after vl-convert padding)\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact 3200×1800 canvas with PAGE_BG fill; never crop (AR-09)\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}