Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Process Capability Analysis

Process capability answers a different question from process behavior charts. Charts ask “Is this process stable?” Capability asks “Can this process consistently meet specifications?”

Wheeler & Bishop (Chapter 16) define two families of capability indices:

IndexMeasuresBased on
Pp / PpkCurrent capability — how the process IS performingOverall sigma (all variation)
Cp / CpkPotential capability — what’s achievable by removing assignable causesR2 residual sigma (within-subgroup variation only)

The gap between Ppk and Cpk tells you how much improvement is available by addressing the assignable causes found in your VAS residual charts.

What You’ll Learn

  1. Assess capability against specification limits

  2. Interpret Pp/Ppk (current) vs Cp/Cpk (potential)

  3. Use one-sided specs

  4. Compare multiple specs against the same study

  5. Understand when Cp/Cpk are unavailable

  6. Visualize capability with .plot()

Setup

We use Bishop’s PBTESTDATABASE_T100 — a 4000-row validation dataset with 4 factor levels x 2 factor levels x 100 time periods, and response columns designed to exercise each Design State (DS).

from processbehavior import ProcessBehavior

# Load the PBTESTDATABASE_T100
pb = ProcessBehavior.read_csv('../../validation/PBTESTDATABASE_T100.csv')

print(f"Dataset: {pb.data.shape[0]} rows x {pb.data.shape[1]} columns")
print(f"Columns: {pb.data.columns.tolist()}")
Found 11655 garbage/NA values across 5 column(s):
  • PM SDS 2: 3200 values
  • PM SDS 3: 958 values
  • PM SDS 4: 2134 values
  • PM SDS 5: 3359 values
  • PM SDS 6: 2004 values

These values were converted to NA and will be excluded from analysis.
Dataset: 4000 rows x 11 columns
Columns: ['PRODUCTION TIME', 'FACTOR 1', 'FACTOR 2', 'FACTOR 1xFACTOR 2', 'PM SDS 1', 'PM SDS 2', 'PM SDS 3', 'PM SDS 4', 'PM SDS 5', 'PM SDS 6', 'PM INERT']
# Formulate a DS 1 study (full replication — has VAS residuals)
study = pb.formulate(
    response='PM SDS 1',
    factors=['FACTOR 1', 'FACTOR 2'],
    time='PRODUCTION TIME',
    precision=3
)

print(study)
Study(response='PM SDS 1', factors=[FACTOR 1, FACTOR 2], time='PRODUCTION TIME', ads=1)
  Valid: Histogram, Xbar, S, X, mR | Recommended: Xbar
  Residuals: R1, R2, R3, R4, R5, R6
  → study.execute() or study.support for details

Basic Capability Assessment

Suppose engineering specifications require the response to be between 233 and 243, with a target of 238. Call study.capability() with your spec limits:

cap = study.capability(usl=243, lsl=233, target=238)
cap
CapabilityResult: Specs: LSL=233, USL=243, Target=238 N=4000, Ybar=237.817, S=1.74, sigma_hat=1.74 Current Capability (overall sigma): Pp=0.958 Ppk=0.923 (lower=0.923, upper=0.993) Potential Capability (R2 sigma): Cp=2.077 Cpk=2.001 (lower=2.001, upper=2.153) Z-scores: Z_lower=2.768, Z_upper=2.978 Empirical (current): Below LSL: 46 (1.15%) Above USL: 1 (0.025%) Total outside: 47 (1.175%) Empirical (potential): Below LSL: 0 (0.0%) Above USL: 0 (0.0%) Note: Stability not assessed; run study.execute() and review signals before interpreting indices.

Reading the Output

Current Capability (Pp/Ppk) uses the overall standard deviation — all sources of variation included:

  • Pp = (USL - LSL) / 6σ^\hat{\sigma} — the spread of the spec window relative to 6 sigma

  • Ppk = min(Ppk_lower, Ppk_upper) — accounts for how centered the process is

  • When Ppk < Pp, the process mean is off-center

Potential Capability (Cp/Cpk) uses only the R2 residual sigma — the within-subgroup variation after removing factor and time effects:

  • If Cpk >> Ppk, significant improvement is possible by addressing assignable causes

  • If Cpk \approx Ppk, the process variation is dominated by noise — improvements require fundamentally changing the process

Empirical counts show how many observations actually fell outside specs — no distributional assumptions, just counting.

Visualizing Capability

.plot() produces a histogram with spec limits (crimson), natural process limits (green dashed), out-of-spec shading, and a capability index box:

# Pass the response values — CapabilityResult is a lean dataclass that doesn't store raw data
cap.plot(values=study.dataset['PM SDS 1'])
Loading...

Accessing Individual Fields

CapabilityResult is a frozen dataclass. All fields are available as attributes with full raw precision:

print(f"Process mean:      {cap.y_bar:.4f}")
print(f"Overall sigma:     {cap.sigma_hat:.4f}")
print(f"R2 sigma:          {cap.sigma_hat_r2:.4f}")
print()
print(f"Pp  = {cap.pp:.3f}    Ppk = {cap.ppk:.3f}")
print(f"Cp  = {cap.cp:.3f}    Cpk = {cap.cpk:.3f}")
print()
print(f"Outside specs: {cap.n_outside} of {cap.n} ({cap.pct_outside:.2f}%)")
Process mean:      237.8169
Overall sigma:     1.7404
R2 sigma:          0.8025

Pp  = 0.958    Ppk = 0.923
Cp  = 2.077    Cpk = 2.001

Outside specs: 47 of 4000 (1.17%)

The as_dict() Method

For programmatic use or export, as_dict() returns a rounded dictionary. You can override the rounding precision:

cap.as_dict(round_to=4)
{'usl': 243, 'lsl': 233, 'target': 238, 'n': 4000, 'y_bar': 237.8169, 's': 1.7403, 'sigma_hat': np.float64(1.7404), 'pp': np.float64(0.9576), 'ppk_lower': np.float64(0.9225), 'ppk_upper': np.float64(0.9927), 'ppk': np.float64(0.9225), 'sigma_hat_r2': np.float64(0.8025), 'cp': np.float64(2.0768), 'cpk_lower': np.float64(2.0007), 'cpk_upper': np.float64(2.1529), 'cpk': np.float64(2.0007), 'potential_unavailable_reason': None, 'z_lower': np.float64(2.7676), 'z_upper': np.float64(2.9781), 'n_below_lsl': 46, 'n_above_usl': 1, 'n_outside': 47, 'pct_below_lsl': 1.15, 'pct_above_usl': 0.025, 'pct_outside': 1.175, 'potential_n_below_lsl': 0, 'potential_n_above_usl': 0, 'potential_pct_below_lsl': 0.0, 'potential_pct_above_usl': 0.0}

Using SpecLimits Directly

For reusable spec definitions, create a SpecLimits object and pass it to capability():

from processbehavior import SpecLimits

customer_specs = SpecLimits(usl=243, lsl=233, target=238)

# Pass to capability()
cap = study.capability(customer_specs)
print(f"Ppk = {cap.ppk:.3f}")
Ppk = 0.923

Comparing Multiple Specifications

capability() is cheap — it reuses the already-computed Study without re-formulation. This makes it easy to evaluate different spec scenarios:

scenarios = [
    ("Tight specs",  SpecLimits(usl=241, lsl=235)),
    ("Normal specs", SpecLimits(usl=243, lsl=233)),
    ("Wide specs",   SpecLimits(usl=245, lsl=231)),
]

print(f"{'Scenario':<16} {'Pp':>6} {'Ppk':>6} {'Cp':>6} {'Cpk':>6} {'% Outside':>10}")
print("-" * 58)
for name, specs in scenarios:
    c = study.capability(specs)
    print(f"{name:<16} {c.pp:6.3f} {c.ppk:6.3f} {c.cp:6.3f} {c.cpk:6.3f} {c.pct_outside:9.2f}%")
Scenario             Pp    Ppk     Cp    Cpk  % Outside
----------------------------------------------------------
Tight specs       0.575  0.539  1.246  1.170      7.80%
Normal specs      0.958  0.923  2.077  2.001      1.17%
Wide specs        1.341  1.306  2.908  2.831      0.15%

Notice how Cp/Cpk (potential) remains high across all scenarios — the within-subgroup variation is small. The gap between Ppk and Cpk shows the improvement available by addressing assignable causes.

One-Sided Specifications

Some processes only have a limit on one side — e.g. a maximum impurity level or minimum strength. Provide only usl or lsl:

# USL only — e.g. maximum allowable measurement
cap_upper = study.capability(usl=242)
print("USL-only:")
print(f"  Pp  = {cap_upper.pp}  (None — not defined for one-sided)")
print(f"  Ppk = {cap_upper.ppk:.3f}  (= Ppk_upper)")
print(f"  Ppk_lower = {cap_upper.ppk_lower}")
print(f"  Ppk_upper = {cap_upper.ppk_upper:.3f}")
print()

# LSL only — e.g. minimum required measurement
cap_lower = study.capability(lsl=234)
print("LSL-only:")
print(f"  Ppk = {cap_lower.ppk:.3f}  (= Ppk_lower)")
USL-only:
  Pp  = None  (None — not defined for one-sided)
  Ppk = 0.801  (= Ppk_upper)
  Ppk_lower = None
  Ppk_upper = 0.801

LSL-only:
  Ppk = 0.731  (= Ppk_lower)

For one-sided specs:

  • Pp and Cp are None (they require both limits to define the spec window)

  • Ppk equals the relevant side — ppk_upper for USL-only, ppk_lower for LSL-only

  • The irrelevant side’s fields are None, so cap.ppk works without branching

When Cp/Cpk Are Unavailable

Potential capability (Cp/Cpk) requires R2 residuals from the Variance Analysis System. R2 is only computed when the study has both factors and time (DS 1-6). If either is missing, the result explains why:

# Formulate without time — no VAS residuals
study_no_time = pb.formulate(
    response='PM SDS 1',
    factors=['FACTOR 1', 'FACTOR 2'],
)

cap_no_r2 = study_no_time.capability(usl=243, lsl=233)
print(f"Cp:  {cap_no_r2.cp}")
print(f"Cpk: {cap_no_r2.cpk}")
print(f"Reason: {cap_no_r2.potential_unavailable_reason}")
print()
print("Current capability (Pp/Ppk) is still available:")
print(f"Pp = {cap_no_r2.pp:.3f}, Ppk = {cap_no_r2.ppk:.3f}")
Cp:  None
Cpk: None
Reason: R2 residuals not available (ADS 1); Cp/Cpk require VAS residuals from a factorial+time design

Current capability (Pp/Ppk) is still available:
Pp = 0.958, Ppk = 0.923

Z-Scores

Z-scores express the distance from the process mean to each spec limit in sigma units. They are algebraically equivalent to 3 x Ppk:

cap = study.capability(usl=243, lsl=233)

print(f"Z_lower = {cap.z_lower:.3f}  (= 3 x Ppk_lower = 3 x {cap.ppk_lower:.3f} = {3 * cap.ppk_lower:.3f})")
print(f"Z_upper = {cap.z_upper:.3f}  (= 3 x Ppk_upper = 3 x {cap.ppk_upper:.3f} = {3 * cap.ppk_upper:.3f})")
Z_lower = 2.768  (= 3 x Ppk_lower = 3 x 0.923 = 2.768)
Z_upper = 2.978  (= 3 x Ppk_upper = 3 x 0.993 = 2.978)

Stability Warning

Capability indices assume the process is stable. If the process has assignable causes (signals on the behavior chart), the indices may be misleading. The result includes a reminder:

print(f"Stability evaluated: {cap.stability_evaluated}")
print()
print("Always run study.execute() first and review the process behavior")
print("chart for signals before interpreting capability indices.")
Stability evaluated: False

Always run study.execute() first and review the process behavior
chart for signals before interpreting capability indices.

The Equations

For reference, here are the Bishop Chapter 16 equations implemented by capability():

Current Capability (overall sigma):

S=std(Y,ddof=1)S = \text{std}(Y, \text{ddof}=1)
σ^=S/c4(N)\hat{\sigma} = S \,/\, c_4(N)
Pp=USLLSL6σ^P_p = \frac{USL - LSL}{6\hat{\sigma}}
Ppk=min ⁣(YˉLSL3σ^,  USLYˉ3σ^)P_{pk} = \min\!\left(\frac{\bar{Y} - LSL}{3\hat{\sigma}},\; \frac{USL - \bar{Y}}{3\hat{\sigma}}\right)

Potential Capability (R2 residual sigma):

SR2=std(R2,ddof=1)S_{R2} = \text{std}(R_2, \text{ddof}=1)
σ^R2=SR2/c4(NR2)\hat{\sigma}_{R2} = S_{R2} \,/\, c_4(N_{R2})
Cp=USLLSL6σ^R2C_p = \frac{USL - LSL}{6\hat{\sigma}_{R2}}
Cpk=min ⁣(YˉLSL3σ^R2,  USLYˉ3σ^R2)C_{pk} = \min\!\left(\frac{\bar{Y} - LSL}{3\hat{\sigma}_{R2}},\; \frac{USL - \bar{Y}}{3\hat{\sigma}_{R2}}\right)

Note: Yˉ\bar{Y} in the Cpk formula is the overall response mean, not the mean of R2 (which is approximately zero).

Summary

ConceptKey Point
study.capability(usl=, lsl=)Assess capability — no re-formulation needed
Pp / PpkCurrent performance (overall sigma)
Cp / CpkPotential performance (R2 sigma) — requires factors + time
Ppk vs Cpk gapImprovement available by removing assignable causes
One-sidedProvide only usl or lsl; Pp/Cp become None
SpecLimitsReusable spec object for multiple assessments
Z-scoresDistance to spec in sigma units (= 3 x Ppk)
cap.plot(values)Histogram with spec limits, NPL overlay, and index box
StabilityAlways check behavior charts before interpreting indices

Next Steps