Weibull Probability Plot for Reliability Analysis — lets-plot

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 lets-plot

Python source (lets-plot)

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

# ruff: noqa: F403, F405, E402
"""anyplot.ai
probability-weibull: Weibull Probability Plot for Reliability Analysis
Library: letsplot | Python 3.13
Quality: pending | Created: 2026-06-07
"""

import os

import numpy as np
import pandas as pd
from lets_plot import *
from lets_plot.export import ggsave
from scipy import stats


LetsPlot.setup_html()

# Theme tokens (Imprint palette — see prompts/default-style-guide.md)
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"

# Imprint palette — categorical (theme-independent)
BRAND = "#009E73"  # position 1 — Failure observations
COLOR_CENSORED = "#C475FD"  # position 2 — Censored observations
COLOR_ETA = "#4467A3"  # position 3 — characteristic life marker

# Data — Bearing wear-out fatigue: shape=3.2 (steep wear-out mode), scale=8000 h
np.random.seed(42)
shape_param = 3.2  # beta: wear-out failure mode (high shape → tight failure band)
scale_param = 8000  # eta: characteristic life in hours
n_failures = 25
n_censored = 8

failure_times = np.sort(stats.weibull_min.rvs(shape_param, scale=scale_param, size=n_failures))
censored_times = np.sort(np.random.uniform(3000, 10000, n_censored))

all_times = np.concatenate([failure_times, censored_times])
is_failure = np.concatenate([np.ones(n_failures), np.zeros(n_censored)])

sort_idx = np.argsort(all_times)
all_times = all_times[sort_idx]
is_failure = is_failure[sort_idx]

# Median rank plotting positions (Bernard's approximation): (i - 0.3) / (n + 0.4)
n_total = len(all_times)
failure_rank = np.cumsum(is_failure)
median_ranks = np.clip((failure_rank - 0.3) / (n_total + 0.4), 1e-6, 1 - 1e-6)

# Weibull linearization: y = ln(-ln(1 - F))
weibull_y = np.log(-np.log(1 - median_ranks))
log_times = np.log(all_times)

df_all = pd.DataFrame(
    {"log_time": log_times, "weibull_y": weibull_y, "status": np.where(is_failure == 1, "Failure", "Censored")}
)

df_failures = df_all[df_all["status"] == "Failure"].copy()

# Linear fit on Weibull-linearized failure points: beta = slope, eta = exp(-intercept/slope)
slope, intercept, r_value, _, _ = stats.linregress(df_failures["log_time"], df_failures["weibull_y"])
beta_fit = slope
eta_fit = np.exp(-intercept / slope)

# Fitted regression line spanning data range with padding
fit_x = np.linspace(np.log(all_times.min() * 0.6), np.log(all_times.max() * 1.4), 100)
fit_y = slope * fit_x + intercept
df_fit = pd.DataFrame({"log_time": fit_x, "weibull_y": fit_y})

# 63.2% characteristic life reference (where Weibull CDF = 1 - 1/e ≈ 0.632)
ref_y = np.log(-np.log(1 - 0.632))  # ≈ 0.0
log_eta = np.log(eta_fit)

# Y-axis ticks — Weibull probability scale
prob_levels = [0.01, 0.05, 0.10, 0.20, 0.50, 0.632, 0.90, 0.99]
y_ticks = [np.log(-np.log(1 - p)) for p in prob_levels]
y_labels = ["1%", "5%", "10%", "20%", "50%", "63.2%", "90%", "99%"]

# X-axis ticks — hours on log scale
x_vals = [1000, 2000, 4000, 6000, 8000, 12000]
x_ticks = [np.log(v) for v in x_vals]
x_labels = ["1,000", "2,000", "4,000", "6,000", "8,000", "12,000"]

# Crosshair segments emphasising the characteristic life intersection
df_h_seg = pd.DataFrame({"x": [log_eta - 0.4], "xend": [log_eta + 0.4], "y": [ref_y], "yend": [ref_y]})
df_v_seg = pd.DataFrame({"x": [log_eta], "xend": [log_eta], "y": [ref_y - 0.28], "yend": [ref_y + 0.28]})

# Characteristic life label — positioned above the crosshair to avoid data crowding
df_eta_label = pd.DataFrame({"x": [log_eta], "y": [ref_y + 0.55], "label": [f"η = {eta_fit:.0f} hrs\n(63.2%)"]})

# Parameter annotation — upper-left (high-probability region is sparse for wear-out data)
df_annot = pd.DataFrame(
    {
        "x": [x_ticks[0] + 0.1],
        "y": [y_ticks[-2] - 0.1],
        "label": [f"β = {beta_fit:.2f}\nη = {eta_fit:.0f} hrs\nR² = {r_value**2:.4f}"],
    }
)

TITLE = "probability-weibull · python · letsplot · anyplot.ai"

anyplot_theme = theme(
    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
    panel_background=element_rect(fill=PAGE_BG),
    panel_border=element_blank(),
    panel_grid_major=element_line(color=INK_MUTED, size=0.25),
    panel_grid_minor=element_blank(),
    axis_title=element_text(color=INK, size=12),
    axis_text=element_text(color=INK_SOFT, size=10),
    axis_line=element_line(color=INK_SOFT),
    plot_title=element_text(color=INK, size=16),
    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
    legend_text=element_text(color=INK_SOFT, size=10),
    legend_title=element_text(color=INK, size=10),
    legend_position=[0.05, 0.17],
)

# Plot
plot = (
    ggplot(df_all, aes(x="log_time", y="weibull_y", color="status", shape="status"))
    # Fitted regression line
    + geom_line(
        data=df_fit, mapping=aes(x="log_time", y="weibull_y"), color=INK, size=1.6, alpha=0.90, inherit_aes=False
    )
    # 63.2% horizontal reference
    + geom_hline(yintercept=ref_y, linetype="dashed", color=INK_MUTED, size=0.6)
    # Vertical reference at eta
    + geom_vline(xintercept=log_eta, linetype="dotted", color=INK_MUTED, size=0.6)
    # Crosshair emphasis at characteristic life intersection
    + geom_segment(
        data=df_h_seg,
        mapping=aes(x="x", y="y", xend="xend", yend="yend"),
        color=COLOR_ETA,
        size=1.8,
        alpha=0.8,
        inherit_aes=False,
    )
    + geom_segment(
        data=df_v_seg,
        mapping=aes(x="x", y="y", xend="xend", yend="yend"),
        color=COLOR_ETA,
        size=1.8,
        alpha=0.8,
        inherit_aes=False,
    )
    # Failure / censored data points (color + shape redundancy for accessibility)
    + geom_point(size=5, alpha=0.9, stroke=1.0)
    # Diamond marker at characteristic life intersection
    + geom_point(
        data=pd.DataFrame({"x": [log_eta], "y": [ref_y]}),
        mapping=aes(x="x", y="y"),
        color=COLOR_ETA,
        fill=COLOR_ETA,
        size=8,
        shape=18,
        alpha=0.95,
        inherit_aes=False,
    )
    # Characteristic life label above intersection to avoid crowding
    + geom_text(
        data=df_eta_label,
        mapping=aes(x="x", y="y", label="label"),
        size=5,
        color=COLOR_ETA,
        hjust=0.5,
        fontface="bold",
        inherit_aes=False,
    )
    # Weibull parameter annotation (upper-left, away from dense data region)
    + geom_text(
        data=df_annot,
        mapping=aes(x="x", y="y", label="label"),
        size=6,
        color=INK_SOFT,
        hjust=0,
        fontface="bold",
        inherit_aes=False,
    )
    + scale_color_manual(name="Observation", values={"Failure": BRAND, "Censored": COLOR_CENSORED})
    + scale_shape_manual(name="Observation", values={"Failure": 16, "Censored": 1})
    + scale_x_continuous(breaks=x_ticks, labels=x_labels)
    + scale_y_continuous(breaks=y_ticks, labels=y_labels)
    + labs(x="Time to Failure (hours)", y="Cumulative Failure Probability", title=TITLE)
    + ggsize(800, 450)
    + anyplot_theme
)

# Save
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")

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

Other implementations