Chris Parmer — home

Process-capability histogram with Cp/Cpk

Manufacturing — NIST MPC61 dataset

Example from the compendium of canonical charts

Manufacturing — Process-capability histogram with Cp/Cpk (NIST MPC61 dataset)

Python Code

"""Manufacturing — Process-capability histogram with Cp/Cpk (NIST MPC61 dataset)."""


import io
import numpy as np
from scipy import stats
import plotly.graph_objects as go
import requests
import warnings

warnings.filterwarnings("ignore")

URL = "https://www.itl.nist.gov/div898/handbook/datasets/MPC61.DAT"
LSL = 97.5
USL = 102.5


def generate():
    print("fetching MPC61.DAT from NIST …")
    r = requests.get(URL, verify=False, timeout=60)
    r.raise_for_status()

    # Parse: skip header lines (non-numeric), extract column 6 (0-indexed)
    data_vals = []
    for line in r.text.splitlines():
        line = line.strip()
        if not line:
            continue
        parts = line.split()
        # Try to parse first token as float
        try:
            float(parts[0])
        except (ValueError, IndexError):
            continue
        # We have a numeric line — take column index 6 if available
        if len(parts) >= 7:
            try:
                data_vals.append(float(parts[6]))
            except ValueError:
                pass
        elif len(parts) > 0:
            # fallback: use first column
            try:
                data_vals.append(float(parts[0]))
            except ValueError:
                pass

    print(f"parsed {len(data_vals)} data points, first few: {data_vals[:5]}")

    if len(data_vals) < 5:
        raise ValueError("Not enough data points parsed from NIST file")

    data = np.array(data_vals)
    mu    = data.mean()
    sigma = data.std(ddof=1)

    Cp  = (USL - LSL) / (6 * sigma)
    Cpk = min((USL - mu) / (3 * sigma), (mu - LSL) / (3 * sigma))

    # Fit normal curve
    x_fit = np.linspace(data.min() - sigma, data.max() + sigma, 300)
    y_fit = stats.norm.pdf(x_fit, mu, sigma)

    # Scale normal pdf to match histogram counts
    n_bins = 20
    bin_width = (data.max() - data.min()) / n_bins
    y_fit_scaled = y_fit * len(data) * bin_width

    fig = go.Figure()

    fig.add_trace(go.Histogram(
        x=data,
        nbinsx=n_bins,
        name="Resistivity",
        marker_color=VIOLET,
        marker_opacity=0.7,
        hovertemplate="Value: %{x:.2f}<br>Count: %{y}<extra></extra>",
    ))

    fig.add_trace(go.Scatter(
        x=x_fit,
        y=y_fit_scaled,
        mode="lines",
        name="Fitted normal",
        line=dict(color=TEAL, width=2.5),
        hovertemplate="x=%{x:.2f}<extra>Normal fit</extra>",
    ))

    # Spec limits
    fig.add_vline(x=LSL, line=dict(color=PINK, width=2, dash="dash"),
                  annotation_text=f"LSL={LSL}", annotation_position="top right",
                  annotation_font=dict(size=11, color=PINK))
    fig.add_vline(x=USL, line=dict(color=PINK, width=2, dash="dash"),
                  annotation_text=f"USL={USL}", annotation_position="top left",
                  annotation_font=dict(size=11, color=PINK))
    fig.add_vline(x=mu, line=dict(color=GREEN, width=1.5, dash="dot"),
                  annotation_text=f"μ={mu:.2f}", annotation_position="top right",
                  annotation_font=dict(size=11, color=GREEN))

    # Cp / Cpk annotation
    fig.add_annotation(
        x=0.98, y=0.95,
        xref="paper", yref="paper",
        text=f"<b>Cp = {Cp:.3f}</b><br><b>Cpk = {Cpk:.3f}</b>",
        showarrow=False,
        align="right",
        font=dict(size=13),
        bgcolor="rgba(255,255,255,0.8)",
        bordercolor=MUTED,
        borderwidth=1,
    )

    fig.update_layout(
        xaxis=dict(title="Resistivity"),
        yaxis=dict(title="Count"),
        legend=dict(orientation="h", y=-0.14),
        margin=dict(t=50, b=70, l=70, r=40),
    )
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly