Weibull Probability Plot for Reliability Analysis — Plotly

A Weibull probability plot displays failure or lifetime data on Weibull probability paper (logarithmic x-axis for time/cycles, linearized Weibull CDF on y-axis) with a fitted straight line. It is the standard tool in reliability engineering for estimating Weibull distribution parameters (shape and scale), assessing whether data follow a Weibull distribution, and extrapolating failure probabilities. The slope of the fitted line gives the shape parameter (beta), while the characteristic life (eta) is read at the 63.2% failure probability crossing.

Weibull Probability Plot for Reliability Analysis rendered with Plotly

Python source (Plotly)

""" anyplot.ai
probability-weibull: Weibull Probability Plot for Reliability Analysis
Library: plotly 6.8.0 | Python 3.13.13
Quality: 88/100 | Updated: 2026-06-07
"""

import os
import sys


# Script name matches the library — remove script dir to avoid shadowing the package
_script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p) != _script_dir]

import numpy as np
import plotly.graph_objects as go
from scipy import stats


# Theme-adaptive chrome — Imprint palette
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
GRID = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)"
GRID_MINOR = "rgba(26,26,23,0.07)" if THEME == "light" else "rgba(240,239,232,0.07)"

# Imprint palette — positions 1 & 2
FAILURE_COLOR = "#009E73"  # brand green — failures / fit line
CENSORED_COLOR = "#C475FD"  # lavender — censored/suspended

font_family = "Helvetica Neue, Helvetica, Arial, sans-serif"

# Data — turbine blade fatigue life (hours)
np.random.seed(42)
n_failures = 25
n_censored = 5
n_total = n_failures + n_censored

shape_true = 2.5
scale_true = 5000
failure_times = np.sort(stats.weibull_min.rvs(shape_true, scale=scale_true, size=n_failures))
censored_times = np.sort(np.random.uniform(1000, 4500, size=n_censored))

all_times = np.concatenate([failure_times, censored_times])
is_censored = np.concatenate([np.zeros(n_failures, dtype=bool), np.ones(n_censored, dtype=bool)])
sort_idx = np.argsort(all_times)
all_times = all_times[sort_idx]
is_censored = is_censored[sort_idx]

# Median rank plotting positions for failures only
failure_ranks = np.zeros(n_total)
rank = 0
for i in range(n_total):
    if not is_censored[i]:
        rank += 1
        failure_ranks[i] = (rank - 0.3) / (n_failures + 0.4)

failure_mask = ~is_censored
failure_t = all_times[failure_mask]
failure_prob = failure_ranks[failure_mask]

# Weibull linearization: y = ln(-ln(1-F))
weibull_y_failures = np.log(-np.log(1 - failure_prob))

# Fit line in Weibull space
slope, intercept, r_value, _, _ = stats.linregress(np.log(failure_t), weibull_y_failures)
beta_fit = slope
eta_fit = np.exp(-intercept / beta_fit)

# Probability axis ticks (Weibull paper y-axis)
prob_ticks = [0.01, 0.02, 0.05, 0.10, 0.20, 0.50, 0.632, 0.90, 0.95, 0.99]
prob_labels = ["1%", "2%", "5%", "10%", "20%", "50%", "63.2%", "90%", "95%", "99%"]
weibull_tick_vals = [np.log(-np.log(1 - p)) for p in prob_ticks]

# Confidence band (90% bounds for fitted line)
t_range = np.logspace(np.log10(failure_t.min() * 0.5), np.log10(failure_t.max() * 1.5), 200)
fit_weibull_y = beta_fit * np.log(t_range) - beta_fit * np.log(eta_fit)
se_fit = np.sqrt(
    np.sum((weibull_y_failures - (beta_fit * np.log(failure_t) - beta_fit * np.log(eta_fit))) ** 2) / (n_failures - 2)
)
conf_upper = fit_weibull_y + 1.645 * se_fit
conf_lower = fit_weibull_y - 1.645 * se_fit

# Figure
fig = go.Figure()

# 90% confidence band (trace 0 — toggleable via update menu)
band_fill = "rgba(0,158,115,0.10)" if THEME == "light" else "rgba(0,158,115,0.16)"
fig.add_trace(
    go.Scatter(
        x=np.concatenate([t_range, t_range[::-1]]),
        y=np.concatenate([conf_upper, conf_lower[::-1]]),
        fill="toself",
        fillcolor=band_fill,
        line={"width": 0},
        name="90% Confidence Band",
        hoverinfo="skip",
        showlegend=True,
    )
)

# Fitted line (trace 1)
fig.add_trace(
    go.Scatter(
        x=t_range,
        y=fit_weibull_y,
        mode="lines",
        name="Weibull Fit",
        line={"color": FAILURE_COLOR, "width": 3},
        hovertemplate="Time: %{x:.0f}h<br>Probability: %{customdata:.1%}<extra>Weibull Fit</extra>",
        customdata=1 - np.exp(-np.exp(fit_weibull_y)),
    )
)

# Failure data points (trace 2)
fig.add_trace(
    go.Scatter(
        x=failure_t,
        y=weibull_y_failures,
        mode="markers",
        name="Failures",
        marker={"size": 14, "color": FAILURE_COLOR, "line": {"color": PAGE_BG, "width": 2}, "opacity": 0.9},
        hovertemplate=(
            "<b>Failure #%{text}</b><br>Time: %{x:.0f} hours<br>Cum. Probability: %{customdata:.1%}<extra></extra>"
        ),
        customdata=failure_prob,
        text=[f"{i + 1}/{n_failures}" for i in range(n_failures)],
    )
)

# Censored data points (trace 3)
censored_t = all_times[is_censored]
censored_weibull_y = beta_fit * np.log(censored_t) - beta_fit * np.log(eta_fit)
censored_prob_est = 1 - np.exp(-np.exp(censored_weibull_y))
fig.add_trace(
    go.Scatter(
        x=censored_t,
        y=censored_weibull_y,
        mode="markers",
        name="Censored (suspended)",
        marker={
            "size": 14,
            "color": "rgba(196,117,253,0.15)",
            "line": {"color": CENSORED_COLOR, "width": 2.5},
            "symbol": "diamond",
        },
        hovertemplate=(
            "<b>Censored Observation</b><br>Time: %{x:.0f} hours<br>Est. Probability: %{customdata:.1%}<extra></extra>"
        ),
        customdata=censored_prob_est,
    )
)

# 63.2% characteristic life reference line
weibull_632 = np.log(-np.log(1 - 0.632))
fig.add_hline(
    y=weibull_632,
    line_dash="dot",
    line_color=INK_MUTED,
    line_width=1.5,
    annotation_text=f"63.2% — η ≈ {eta_fit:.0f}h",
    annotation_position="bottom left",
    annotation_font={"size": 10, "color": INK_MUTED, "family": font_family},
)
fig.add_vline(x=eta_fit, line_dash="dot", line_color=INK_MUTED, line_width=1, opacity=0.5)

# Parameter box (lower-right corner, paper coordinates to avoid log-scale issues)
annot_bg = "rgba(255,253,246,0.93)" if THEME == "light" else "rgba(36,36,32,0.93)"
annot_border = "rgba(0,158,115,0.35)"
fig.add_annotation(
    x=0.98,
    y=0.05,
    xref="paper",
    yref="paper",
    text=(
        f"<b>Weibull Parameters</b><br>"
        f"β (shape) = {beta_fit:.2f}<br>"
        f"η (scale) = {eta_fit:.0f}h<br>"
        f"R² = {r_value**2:.4f}"
    ),
    showarrow=False,
    font={"size": 11, "color": INK, "family": font_family},
    align="left",
    bgcolor=annot_bg,
    bordercolor=annot_border,
    borderwidth=1.5,
    borderpad=10,
    xanchor="right",
    yanchor="bottom",
)

# B10 life annotation — points to actual data coordinate
b10_life = eta_fit * (-np.log(1 - 0.10)) ** (1 / beta_fit)
weibull_10 = np.log(-np.log(1 - 0.10))
fig.add_annotation(
    x=b10_life,
    y=weibull_10,
    xref="x",
    yref="y",
    text=f"B10 = {b10_life:.0f}h",
    showarrow=True,
    arrowhead=2,
    arrowsize=1,
    arrowcolor=INK_SOFT,
    ax=55,
    ay=28,
    font={"size": 10, "color": INK_MUTED, "family": font_family},
    bgcolor=annot_bg,
    bordercolor="rgba(26,26,23,0.12)" if THEME == "light" else "rgba(240,239,232,0.12)",
    borderwidth=1,
    borderpad=5,
)

# Update menu — toggle confidence band on/off (LM-01 advanced Plotly pattern)
fig.update_layout(
    updatemenus=[
        {
            "type": "buttons",
            "showactive": True,
            "x": 0.99,
            "y": 0.99,
            "xanchor": "right",
            "yanchor": "top",
            "bgcolor": ELEVATED_BG,
            "bordercolor": INK_SOFT,
            "font": {"color": INK_SOFT, "size": 10, "family": font_family},
            "buttons": [
                {"label": "Show Band", "method": "update", "args": [{"visible": [True, True, True, True]}]},
                {"label": "Hide Band", "method": "update", "args": [{"visible": [False, True, True, True]}]},
            ],
        }
    ]
)

# Layout
fig.update_layout(
    autosize=False,
    width=800,
    height=450,
    paper_bgcolor=PAGE_BG,
    plot_bgcolor=PAGE_BG,
    font={"color": INK, "family": font_family},
    template="plotly_white",
    title={
        "text": "probability-weibull · python · plotly · anyplot.ai",
        "font": {"size": 16, "family": font_family, "color": INK},
        "x": 0.5,
        "y": 0.98,
        "xanchor": "center",
        "yanchor": "top",
    },
    xaxis={
        "title": {
            "text": "Time to Failure (hours)",
            "font": {"size": 12, "family": font_family, "color": INK},
            "standoff": 15,
        },
        "tickfont": {"size": 10, "family": font_family, "color": INK_SOFT},
        "type": "log",
        "showgrid": True,
        "gridcolor": GRID,
        "gridwidth": 1,
        "showline": False,
        "minor": {"showgrid": True, "gridcolor": GRID_MINOR},
        "zeroline": False,
    },
    yaxis={
        "title": {
            "text": "Cumulative Failure Probability (Weibull Scale)",
            "font": {"size": 12, "family": font_family, "color": INK},
            "standoff": 10,
        },
        "tickfont": {"size": 10, "family": font_family, "color": INK_SOFT},
        "tickmode": "array",
        "tickvals": weibull_tick_vals,
        "ticktext": prob_labels,
        "showgrid": True,
        "gridcolor": GRID,
        "gridwidth": 1,
        "showline": False,
        "range": [weibull_tick_vals[0] - 0.3, weibull_tick_vals[-1] + 0.3],
        "zeroline": False,
    },
    legend={
        "font": {"size": 10, "family": font_family, "color": INK_SOFT},
        "x": 0.02,
        "y": 0.98,
        "bgcolor": ELEVATED_BG,
        "bordercolor": INK_SOFT,
        "borderwidth": 1,
    },
    margin={"l": 90, "r": 40, "t": 80, "b": 60},
    hoverlabel={"font": {"size": 10, "family": font_family}, "bgcolor": ELEVATED_BG, "bordercolor": FAILURE_COLOR},
)

# Save
fig.write_image(f"plot-{THEME}.png", width=800, height=450, scale=4)
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

Part of Weibull Probability Plot for Reliability Analysis on anyplot.ai.

Other implementations