Standard Process Capability
Executive Summary

Executive Summary

Whether diameter mm is capable of meeting its specification

Measurements
75
Process Mean
5.6136
Sigma (Within)
0.0464
Sigma (Overall)
0.0483
Cp
1.078
Cpk
0.889
Pp
1.035
Ppk
0.853
Expected PPM
5389.65
Observed PPM
0
Process Sigma Level
4.16
Normality
rejected
Verdict
Not Capable (Cpk 0.89)
Diameter Mm runs at a mean of 5.6136 against the specification 5.49 to 5.79. Cpk is 0.89 (95 percent interval 0.73 to 1.05) and Ppk is 0.85, which makes the process not capable against the 1.33 threshold in common use. The lower limit binds: the mean sits 0.1236 away from it, which is 2.67 within-process sigma. Cp is 1.08 and Pp is 1.03; the drop from one to the other is the between-subgroup drift that only a long-term view sees. The normal model puts the long-term defect rate at 5,390 parts per million; 0 units actually measured outside the specification, a rate of 0 parts per million. Normality was tested and rejected, so the indices and the model-based defect rate above should not be trusted as stated — read the observed rate and the normality card before acting on them.
What this means

The short answer

No, the process is not capable. Cpk is 0.889 against the standard 1.33 threshold. The lower specification limit (5.49 mm) is the binding constraint; the mean sits 2.67 within-process sigma away from it.

The detail

Cpk = 0.889 with a 95% confidence interval of 0.727 to 1.050. Ppk = 0.853. In 75 units, 0 measurements fell outside the specification, yielding an observed defect rate of 0 parts per million. The normal model predicts 5,390 parts per million long-term and 3,910 short-term. Process sigma is 2.66 (or 4.16 under the Six Sigma 1.5-shift convention). The gap from Cp (1.078) to Pp (1.035) reflects between-subgroup drift of 0.0136 in standard deviation.

What this can't tell you

Normality was rejected (Anderson-Darling p = 0.0495), so the indices and the normal-model defect rate should not be trusted as stated. The observed zero-defect count cannot resolve rates below roughly 13,333 parts per million. Rely on the observed rate (0 PPM) rather than the modelled estimate (5,390 PPM) until the process is re-evaluated with a larger sample or a non-normal method.

Overview

Analysis Overview

Capability of diameter mm against the specification 5.49 to 5.79, over 75 units.

N Measurements75
Process Mean5.6136
Sigma Within0.0464
Sigma Overall0.0483
Spec Sidestwo-sided
What this means

The short answer

A capability index measures whether your process spread fits inside the specification window. Here, the specification is 0.3 mm wide (5.49 to 5.79), and the process occupies 0.2782 of that window. The process mean sits at 5.6136, which is 0.1236 mm away from the lower limit—the binding constraint. This off-centre position is why Cpk (0.889) is smaller than Cp (1.078): the process has room to spare on the high side but is crowded on the low side.

The detail

Cp divides the specification width by six within-process sigma (0.0464). That yields 0.30 ÷ (6 × 0.0464) = 1.078. Cpk measures only to the nearer limit: (5.6136 − 5.49) ÷ (3 × 0.0464) = 0.889. The centring index k is 0.176, meaning the mean sits 17.6% of the way from mid-spec toward the lower limit. Pp (1.035) and Ppk (0.853) repeat both calculations using the overall standard deviation 0.0483 rather than the within-group spread, capturing the 0.0136 mm of drift between consecutive measurements.

What this can't tell you

The indices assume a normal distribution, which the data rejects (Anderson-Darling p = 0.0495). Treat these as spread descriptors, not defect predictions. The confidence interval on Cpk spans 0.727 to 1.050 at 75 units—wide enough that the second decimal carries limited precision.

Data Preparation

Data Quality

How the rows, the limits and the subgroups were read.

Initial Rows75
Final Rows75
Rows Removed0
N Subgroups0
Distinct Values18
What this means

The short answer

All 75 measurements were usable with no missing values. The specification limits 5.49 and 5.79 were read directly from the data columns. No subgroup structure was available, so short-term variation was estimated from the moving range between consecutive measurements. The process produced 18 distinct measured values.

The detail

75 rows loaded, 75 retained. Lower limit 5.49 from 'lower spec' column; upper limit 5.79 from 'upper spec' column. No target value supplied, so Cpm is not reported. No subgroup column was mapped, so within-process sigma (0.0464) comes from the average moving range divided by 1.128, the Individuals chart estimator. 18 distinct measured values appear in the data.

What this can't tell you

The absence of a subgroup column means the drift component (0.0136) is inferred from residuals rather than attributed to a known source (shift, batch, machine). A subgroup-level export would clarify whether drift is systematic and addressable or inherent to the process.

Visualization

Distribution Against the Specification

Where diameter mm actually falls relative to the limits it is judged against.

What this means

The short answer

The process is squeezed into the lower half of the specification window. All 75 measurements fall between approximately 5.5 and 5.68 mm, leaving a narrow margin above and a narrower one below. The mean at 5.6136 is only 0.1236 mm from the lower limit, consuming 17.6% of the distance from mid-spec to that limit.

The detail

Specification window: 0.3 mm wide. Process occupies 0.2782 mm (six within-process sigma). Centring index k: 0.176. All measurements fall within the specification; none outside. The histogram shows 18 distinct values distributed across the window, with no clear outliers detaching from the bulk. The process mean sits 17.6% of the way from mid-spec toward the lower limit, placing the bulk of the distribution closer to 5.49 than to 5.79.

What this can't tell you

At 75 units, the sample cannot resolve defect rates finer than roughly 13,333 parts per million. The histogram shows no out-of-spec measurements, but that absence reflects sample size, not process perfection. The shape assumption (normality) failed the Anderson-Darling test (p = 0.0495), so the normal-curve indices may mischaracterize the tail behaviour where defects actually occur.

Data Table

Capability Indices

Every index, what it is measuring, and which one binds.

IndexValueInterpretation
Cp1.078Spec width divided by six within-process sigma — the best diameter mm could do if it were perfectly centred
CPU1.268Distance from the mean up to the upper limit, in three-sigma units
CPL0.8886Distance from the mean down to the lower limit, in three-sigma units
Cpk0.8886The smaller of the two sides — short-term capability as actually centred (the lower side binds)
Pp1.035Cp recomputed on total long-term variation instead of within-subgroup variation
Ppk0.8528Cpk recomputed on total long-term variation — what the customer actually receives over time
k0.176Centring index: 0 means the mean sits exactly mid-spec, 1 means it sits on a limit
What this means

The short answer

Cpk is 0.889, which falls short of the 1.33 standard. The lower limit binds: CPU (distance to upper limit in three-sigma units) is 1.268, but CPL (distance to lower limit) is 0.889. The shortfall is 17.6% centring (k = 0.176) and 82.4% spread; most of the problem is the process width, not its position.

The detail

Cp = 1.078 (spec width ÷ 6 within-process sigma). CPL = 0.889 (distance to lower limit ÷ 3 within-process sigma); CPU = 1.268 (distance to upper limit ÷ 3 within-process sigma). Cpk = min(CPL, CPU) = 0.889. Pp = 1.035; Ppk = 0.853. The 95% confidence interval on Cpk is 0.727 to 1.050—a width that reflects the precision limit at 75 units. The centring index k = 0.176 quantifies how far the mean sits from mid-spec: 0 would be perfect centering, 1 would place the mean on a limit.

What this can't tell you

The confidence interval's width (0.323 mm) reminds you that a Cpk quoted to two decimals (0.89) carries less precision than it appears. Normality was rejected, so the interval itself rests on an assumption that does not hold in this data.

Data Table

Defect Rate and Sigma Level

What fell outside the limits, and what the normal model predicts.

StatisticValueBasis
Observed out of spec (PPM)00 of 75 units measured outside the specification
Observed below lower limit (PPM)00 units below 5.49
Observed above upper limit (PPM)00 units above 5.79
Expected out of spec, long-term normal model (PPM)5390normal curve at mean 5.6136 with the overall standard deviation 0.0483
Expected out of spec, short-term normal model (PPM)3910normal curve at mean 5.6136 with the within-process sigma 0.0464
Observed yield (%)100share of the 75 measurements inside the specification
Process sigma, short-term (Z bench)2.66the normal deviate matching the short-term out-of-spec probability
Sigma level with the 1.5 shift convention4.16Z bench plus 1.5, the long-term drift allowance used in Six Sigma reporting
What this means

The short answer

Zero defects were observed in 75 units (0 PPM). The normal model predicts 5,390 parts per million long-term, but because normality was rejected, that forecast is not trustworthy. The observed rate is the safer figure, even though a sample of 75 cannot detect defects below roughly 13,333 PPM.

The detail

Observed out-of-spec: 0 of 75 units = 0 PPM. Expected (long-term normal model): 5,389.65 PPM. Expected (short-term normal model): 3,909.78 PPM. Process sigma (short-term, Z-bench) = 2.66. Sigma level with 1.5 shift convention = 4.16. The short-term model uses within-process sigma 0.0464; the long-term uses overall sigma 0.0483, capturing drift.

What this can't tell you

Normality was rejected (Anderson-Darling p = 0.0495), so the modelled defect rate should not be believed as stated. The observed zero-defect count cannot distinguish between a truly excellent process and one that simply had no unlucky samples; the resolution floor is roughly 13,333 PPM at this sample size. A larger sample or a non-normal method (e.g., Box-Cox transformation) would be needed to estimate the true tail risk.

Visualization

Normality Check

Whether the assumption the indices rest on actually holds.

What this means

The short answer

The data departs from normality. Anderson-Darling rejects at p = 0.0495 (the 5% threshold); Shapiro-Wilk does not (p = 0.1121). The distribution shows slight negative skewness (−0.213) and negative kurtosis (−0.615), meaning it is slightly left-skewed with lighter tails than a normal curve. All capability indices—Cp, Cpk, Pp, Ppk, and the normal-model defect rate—rest on the normality assumption and should not be trusted as stated.

The detail

Shapiro-Wilk p = 0.1121 (does not reject). Anderson-Darling p = 0.0495 (rejects at 5% level). Skewness = −0.213; excess kurtosis = −0.615. The Q-Q plot shows the lower tail slightly above the diagonal and the upper tail slightly below, consistent with a lighter-tailed, left-skewed distribution. No single outlier detaches dramatically; the departure is distributed across both tails.

What this can't tell you

The two normality tests disagree at the margin, reflecting the inherent variability of testing at small samples. Treat this as a warning, not a verdict: the safest approach is to quote the observed defect count (0 PPM) rather than the modelled estimate (5,390 PPM). A transformation of diameter mm or a non-normal capability method would be more honest than the indices as computed.

Visualization

Where the Variation Comes From

Short-term spread versus drift — the difference between Cp and Pp.

What this means

The short answer

The within-subgroup spread (0.0464 mm) is larger than the between-subgroup drift (0.0136 mm). Drift accounts for 7.9% of the total variance. Because no subgroup column was mapped, the drift is inferred from the overall standard deviation (0.0483) rather than attributed to a specific source like shift or batch.

The detail

Within-subgroup (short-term) standard deviation: 0.0464 mm. Between-subgroup (drift) standard deviation: 0.0136 mm. Overall (long-term) standard deviation: 0.0483 mm. Drift is the gap between Cp (1.078) and Pp (1.035). Removing drift entirely would move Pp toward Cp, a practical gain of about 0.043 in the index.

What this can't tell you

The drift component is inferred from the moving range, not mapped to a real source. Mapping an actual subgroup structure (shift, batch, machine, cavity) would show whether the 7.9% variance is systematic and addressable. Without that mapping, it is unclear whether eliminating drift is practical or whether it reflects unmeasured factors. A subgroup-level export would clarify the opportunity.

Rate this report Was this the answer you needed?
The exact source that produced this report — yours to keep, read, and re-run.
Download PDF
How this was computed method · R source · citation
The code that did it

Process Capability — Cp / Cpk

Is the process capable of meeting its specification limits? Given a measured characteristic and the spec limits it is judged against, the analysis computes the short-term indices (Cp, Cpk) from within-subgroup variation, the long-term indices (Pp, Ppk) from total variation, the estimated defect rate in parts per million, and the process sigma level.

Why This Method?

Control charts answer "is the process stable?" — the voice of the process. Capability answers the different question the customer actually asks: "does the output fit inside the window it has to fit in?" — the voice of the customer. Cp is the ratio of the spec width to the natural process width; Cpk penalises a process that is off-centre; Pp and Ppk repeat both using long-term variation, so the gap between them measures drift between subgroups.

What This Analysis Covers

  • The distribution of the characteristic drawn against its spec limits
  • Cp, Cpk, CPU, CPL, Pp, Ppk, Cpm and the centring index k
  • Observed and normal-model defect rates (PPM) and the process sigma level
  • A normality check (Shapiro-Wilk plus a hand-implemented Anderson-Darling),

because the indices assume normality and mislead badly when it fails

  • Within- versus between-subgroup variation, which is exactly the Cp/Pp gap

Standard Library

Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {measurement, lower_spec, upper_spec, subgroup, target}. All narrative is derived from the user's own column names and computed values.

suppressPackageStartupMessages(library(DT))
suppressPackageStartupMessages(library(htmlwidgets))
suppressPackageStartupMessages(library(arrow))
suppressPackageStartupMessages(library(knitr))
suppressPackageStartupMessages(library(rmarkdown))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(tidyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(stringr))
suppressPackageStartupMessages(library(lubridate))
suppressPackageStartupMessages(library(broom))
suppressPackageStartupMessages(library(Matrix))
suppressPackageStartupMessages(library(cluster))
suppressPackageStartupMessages(library(data.table))

Helpers

Core Analysis Pipeline

compute_shared <- function(df, params, col_map = list()) {
  # === SHARED EXPORTS ===
  #   initial_rows/final_rows/rows_removed  $ row accounting
  #   measurement_name / subgroup_name      $ humanized user names
  #   lsl / usl / target / spec_sides       $ resolved specification
  #   lsl_source / usl_source               $ where each limit came from
  #   mu / sd_overall / sigma_within        $ location and the two spreads
  #   sigma_source                          $ how within-sigma was estimated
  #   cp/cpu/cpl/cpk/pp/ppu/ppl/ppk/cpm/k   $ capability indices (NA when undefined)
  #   cpk_ci_low / cpk_ci_high              $ 95% interval on Cpk
  #   ppm_obs_* / ppm_exp_st / ppm_exp_lt   $ defect rates
  #   z_bench_st / z_bench_lt / sigma_level $ sigma level
  #   shapiro_p / ad_p / skewness / kurtosis / normal_ok / normality_text
  #   n_groups / sigma_between / pct_between / anova_p   $ variance sources
  #   n_unstable / stability_text
  #   dist_df / qq_df / indices_df / defects_df / variation_df
  #   metrics / json_output
  # === /SHARED EXPORTS ===

  initial_rows <- nrow(df)
  measurement_name <- humanize_semantic("measurement", col_map)[1]

  if (!("measurement" %in% names(df))) {
    stop(sprintf("A numeric measurement column(&#x27;%s') must be mapped — it is the characteristic whose capability is being judged.",
                 measurement_name))
  }

Step 1: Read the measurement (95% numeric rule) and drop missing rows

mv <- coerce_numeric_95(df$measurement)
  if (is.null(mv)) {
    stop(sprintf("The column &#x27;%s' is not numeric — process capability needs a measured numeric characteristic.",
                 measurement_name))
  }
  keep <- is.finite(mv)
  n_dropped_na <- sum(!keep)
  x <- mv[keep]
  n <- length(x)
  if (n < 20) {
    stop(sprintf("Only %d usable %s of &#x27;%s' — a capability study needs at least 20 measurements to estimate the spread at all (30 or more is the usual practical floor).",
                 n, units_word(n), measurement_name))
  }
  final_rows <- n
  rows_removed <- initial_rows - final_rows

  mu <- mean(x)
  sd_overall <- stats::sd(x)
  if (!is.finite(sd_overall) || sd_overall <= 0) {
    stop(sprintf("Every value of &#x27;%s' is identical, so the process has no measurable spread and no capability index can be computed.",
                 measurement_name))
  }

Step 2: Resolve the specification limits

Three routes, in priority order: module parameters, then a mapped spec-limit column, then nothing (in which case the analysis refuses).

lsl_h <- if ("lower_spec" %in% names(df)) humanize_semantic("lower_spec", col_map)[1] else "lower spec"
  usl_h <- if ("upper_spec" %in% names(df)) humanize_semantic("upper_spec", col_map)[1] else "upper spec"
  tgt_h <- if ("target" %in% names(df)) humanize_semantic("target", col_map)[1] else "target"

  lsl_r <- resolve_spec_limit(params$lsl %||% params$LSL %||% params$lower_spec_limit %||%
                                params$lower_spec %||% params$lower_specification_limit,
                              if ("lower_spec" %in% names(df)) df$lower_spec[keep] else NULL,
                              "lsl", lsl_h)
  usl_r <- resolve_spec_limit(params$usl %||% params$USL %||% params$upper_spec_limit %||%
                                params$upper_spec %||% params$upper_specification_limit,
                              if ("upper_spec" %in% names(df)) df$upper_spec[keep] else NULL,
                              "usl", usl_h)
  tgt_r <- resolve_spec_limit(params$target %||% params$nominal %||% params$target_value,
                              if ("target" %in% names(df)) df$target[keep] else NULL,
                              "target", tgt_h)

  lsl <- lsl_r$value; usl <- usl_r$value; target <- tgt_r$value
  has_lsl <- is.finite(lsl); has_usl <- is.finite(usl)
  if (!has_lsl && !has_usl) {
    stop(sprintf("No specification limit was supplied for &#x27;%s', so Cp and Cpk cannot be computed — capability is a comparison against a spec, and inventing one would invent the answer. Supply a limit in any of three ways: map a column holding the limit to lower_spec or upper_spec, pass lsl or usl as a module parameter, or add a constant limit column to the file.",
                 measurement_name))
  }
  if (has_lsl && has_usl && !(usl > lsl)) {
    stop(sprintf("The upper specification limit(%s) must be greater than the lower specification limit(%s).",
                 fmt_num(usl, 4), fmt_num(lsl, 4)))
  }
  spec_sides <- if (has_lsl && has_usl) "two-sided" else if (has_usl) "upper only" else "lower only"
  if (is.finite(target) && has_lsl && has_usl && (target < lsl || target > usl)) {
    target <- NA_real_   # a target outside the spec window is not usable for Cpm
  }

Step 3: Subgroups (optional) and the two sigma estimates

Within-subgroup (short-term) sigma drives Cp/Cpk; total (long-term) sigma drives Pp/Ppk. The difference between them IS the between-subgroup drift.

has_sub <- "subgroup" %in% names(df)
  subgroup_name <- if (has_sub) humanize_semantic("subgroup", col_map)[1] else NA_character_
  g <- if (has_sub) as.character(df$subgroup)[keep] else NULL
  if (has_sub) {
    g[is.na(g) | !nzchar(trimws(g))] <- "Missing"
  }

  n_groups <- 0L; sigma_between <- NA_real_; pct_between <- NA_real_
  anova_f <- NA_real_; anova_p <- NA_real_
  group_means <- NULL; group_sizes <- NULL
  sigma_within <- NA_real_; sigma_source <- ""

  if (has_sub) {
    sizes <- table(g)
    usable <- names(sizes)[sizes >= 2]
    if (length(usable) >= 2) {
      idx <- g %in% usable
      xg <- x[idx]; gg <- g[idx]
      lev <- unique(gg)
      n_groups <- length(lev)
      group_sizes <- as.integer(table(gg)[lev])
      group_means <- vapply(lev, function(l) mean(xg[gg == l]), numeric(1))
      ss_w <- sum(vapply(lev, function(l) {
        v <- xg[gg == l]; sum((v - mean(v))^2)
      }, numeric(1)))
      df_w <- length(xg) - n_groups
      sigma_within <- sqrt(ss_w / df_w)
      sigma_source <- sprintf("pooled within-subgroup standard deviation across %d subgroups of &#x27;%s'",
                              n_groups, subgroup_name)

One-way analysis of variance, computed directly from sums of squares.

grand <- mean(xg)
      ss_b <- sum(group_sizes * (group_means - grand)^2)
      df_b <- n_groups - 1
      ms_b <- ss_b / df_b; ms_w <- ss_w / df_w
      if (is.finite(ms_w) && ms_w > 0) {
        anova_f <- ms_b / ms_w
        anova_p <- stats::pf(anova_f, df_b, df_w, lower.tail = FALSE)
      }
      n0 <- (length(xg) - sum(group_sizes^2) / length(xg)) / df_b
      if (is.finite(n0) && n0 > 0) {
        var_b <- max(0, (ms_b - ms_w) / n0)
        sigma_between <- sqrt(var_b)
        denom <- var_b + sigma_within^2
        if (is.finite(denom) && denom > 0) pct_between <- 100 * var_b / denom
      }
    }
  }

  if (!is.finite(sigma_within) || sigma_within <= 0) {

No usable subgroups: fall back to the Individuals estimator, the average moving range between consecutive measurements divided by d2 = 1.128.

mr <- abs(diff(x))
    mrbar <- mean(mr)
    sigma_within <- mrbar / 1.128
    sigma_source <- "average moving range between consecutive measurements divided by 1.128 (the Individuals estimator, used because no usable subgroups were mapped)"
    n_groups <- 0L
    if (!is.finite(sigma_within) || sigma_within <= 0) {
      sigma_within <- sd_overall
      sigma_source <- "the overall standard deviation(consecutive measurements repeat exactly, so no short-term estimate was possible)"
    }
  }
  if (!is.finite(sigma_between) || is.na(sigma_between)) {
    sigma_between <- sqrt(max(0, sd_overall^2 - sigma_within^2))
    if (!is.finite(pct_between)) {
      denom <- sigma_between^2 + sigma_within^2
      pct_between <- if (denom > 0) 100 * sigma_between^2 / denom else 0
    }
  }

Step 4: Capability indices

cp  <- if (has_lsl && has_usl) (usl - lsl) / (6 * sigma_within) else NA_real_
  cpu <- if (has_usl) (usl - mu) / (3 * sigma_within) else NA_real_
  cpl <- if (has_lsl) (mu - lsl) / (3 * sigma_within) else NA_real_
  cpk <- min(c(cpu, cpl), na.rm = TRUE)
  pp  <- if (has_lsl && has_usl) (usl - lsl) / (6 * sd_overall) else NA_real_
  ppu <- if (has_usl) (usl - mu) / (3 * sd_overall) else NA_real_
  ppl <- if (has_lsl) (mu - lsl) / (3 * sd_overall) else NA_real_
  ppk <- min(c(ppu, ppl), na.rm = TRUE)
  cpm <- if (has_lsl && has_usl && is.finite(target)) {
    (usl - lsl) / (6 * sqrt(sd_overall^2 + (mu - target)^2))
  } else NA_real_
  k <- if (has_lsl && has_usl) abs(mu - (usl + lsl) / 2) / ((usl - lsl) / 2) else NA_real_
  binding_side <- if (has_lsl && has_usl) {
    if (is.finite(cpu) && is.finite(cpl) && cpu <= cpl) "upper" else "lower"
  } else if (has_usl) "upper" else "lower"

95% interval on Cpk (Bissell's normal approximation). It widens fast as n falls, which is the honest counterweight to quoting Cpk to two decimals.

cpk_se <- sqrt(1 / (9 * n) + cpk^2 / (2 * (n - 1)))
  cpk_ci_low  <- cpk - stats::qnorm(0.975) * cpk_se
  cpk_ci_high <- cpk + stats::qnorm(0.975) * cpk_se

Step 5: Defect rates — what was observed, and what the normal model says

n_below <- if (has_lsl) sum(x < lsl) else 0L
  n_above <- if (has_usl) sum(x > usl) else 0L
  n_out <- n_below + n_above
  ppm_obs_below <- 1e6 * n_below / n
  ppm_obs_above <- 1e6 * n_above / n
  ppm_obs <- 1e6 * n_out / n

  tail_p <- function(sig) {
    lo <- if (has_lsl) stats::pnorm((lsl - mu) / sig) else 0
    hi <- if (has_usl) stats::pnorm((usl - mu) / sig, lower.tail = FALSE) else 0
    c(lo = lo, hi = hi, tot = lo + hi)
  }
  p_st <- tail_p(sigma_within)
  p_lt <- tail_p(sd_overall)
  ppm_exp_st <- 1e6 * p_st[["tot"]]
  ppm_exp_lt <- 1e6 * p_lt[["tot"]]
  ppm_exp_lt_below <- 1e6 * p_lt[["lo"]]
  ppm_exp_lt_above <- 1e6 * p_lt[["hi"]]

  z_of <- function(p_tot) {
    p_tot <- min(max(p_tot, 1e-16), 1 - 1e-16)
    stats::qnorm(p_tot, lower.tail = FALSE)
  }
  z_bench_st <- z_of(p_st[["tot"]])
  z_bench_lt <- z_of(p_lt[["tot"]])
  sigma_level <- z_bench_st + 1.5      # the industry "six sigma" convention
  yield_obs <- 100 * (n - n_out) / n

Step 6: Normality — the assumption the indices stand on

set.seed(42)
  x_test <- if (n > 5000) sample(x, 5000) else x
  shapiro_n <- length(x_test)
  shapiro_p <- NA_real_
  if (shapiro_n >= 3 && length(unique(x_test)) > 1) {
    sw <- tryCatch(stats::shapiro.test(x_test), error = function(e) NULL)
    if (!is.null(sw)) shapiro_p <- sw$p.value
  }
  ad <- ad_test_normal(x)
  ad_p <- ad$p
  m2 <- mean((x - mu)^2); m3 <- mean((x - mu)^3); m4 <- mean((x - mu)^4)
  skewness <- if (m2 > 0) m3 / m2^1.5 else NA_real_
  kurtosis <- if (m2 > 0) m4 / m2^2 - 3 else NA_real_
  rejects <- c(if (is.finite(shapiro_p) && shapiro_p < 0.05) "Shapiro-Wilk",
               if (is.finite(ad_p) && ad_p < 0.05) "Anderson-Darling")
  normal_ok <- length(rejects) == 0 && (is.finite(shapiro_p) || is.finite(ad_p))

Gauge discrimination: the smallest step the instrument actually recorded. When that step covers a meaningful fraction of the process spread, every normality test is partly reading rounding rather than process shape.

n_distinct <- length(unique(x))
  ux <- sort(unique(x))
  gauge_res <- if (length(ux) >= 2) min(diff(ux)) else NA_real_
  gauge_coarse <- (is.finite(gauge_res) && gauge_res >= sd_overall / 4) || n_distinct < 10

  shapiro_txt <- if (is.finite(shapiro_p)) {
    sprintf("Shapiro-Wilk p = %s%s", fmt_num(shapiro_p, 4),
            if (n > 5000) sprintf(" (computed on a random sample of %s of the %s measurements)",
                                  format(shapiro_n, big.mark = ","), format(n, big.mark = ",")) else "")
  } else "Shapiro-Wilk could not be computed"
  ad_txt <- if (is.finite(ad_p)) sprintf("Anderson-Darling p = %s", fmt_num(ad_p, 4))
            else "Anderson-Darling could not be computed"
  shape_txt <- sprintf("skewness %s, excess kurtosis %s", fmt_num(skewness, 3), fmt_num(kurtosis, 3))

  normality_text <- if (normal_ok) {
    paste0(shapiro_txt, " and ", ad_txt,
           " — neither test rejects normality at the 5 percent level(", shape_txt,
           "). The capability indices rest on a normal distribution, and that assumption survives here.")
  } else if (length(rejects) > 0) {
    paste0(shapiro_txt, " and ", ad_txt, " — ",
           paste(rejects, collapse = " and "),
           if (length(rejects) == 1) " rejects" else " reject",
           " normality at the 5 percent level(", shape_txt,
           "). Cp, Cpk, Pp, Ppk and the normal-model defect rate all assume a normal distribution, so on this data they should not be trusted as stated",
           if (is.finite(skewness) && abs(skewness) > 0.5) {
             sprintf("; the distribution is %s-skewed, which means the %s tail is longer than a normal curve allows and the defect rate on that side is understated",
                     if (skewness > 0) "right" else "left",
                     if (skewness > 0) "upper" else "lower")
           } else "", ".")
  } else {
    paste0("Normality could not be tested on this data(", shape_txt,
           "), so the capability indices should be read as descriptive only.")
  }
  if (gauge_coarse) {
    normality_text <- paste0(normality_text, " Only ", n_distinct,
      " distinct values appear across ", format(n, big.mark = ","),
      " measurements, in steps of ", fmt_num(gauge_res, 6),
      " against an overall standard deviation of ", fmt_num(sd_overall, 4),
      ", so the gauge resolution is coarse relative to the process spread and every normality test is reading rounding as much as shape.")
  }

Step 7: Stability — capability only means something for a stable process

if (n_groups >= 2 && !is.null(group_means)) {
    grand <- mean(x)
    se_g <- sigma_within / sqrt(group_sizes)
    n_unstable <- sum(group_means > grand + 3 * se_g | group_means < grand - 3 * se_g)
    stability_unit <- "subgroup"
  } else {
    n_unstable <- sum(x > mu + 3 * sigma_within | x < mu - 3 * sigma_within)
    stability_unit <- "measurement"
  }
  stability_text <- if (n_unstable == 0) {
    sprintf("No %s falls outside three within-process sigma of the centre, so nothing in this data contradicts the stability that a capability study assumes.",
            stability_unit)
  } else {
    sprintf("%d %s%s fall outside three within-process sigma of the centre. Capability describes a stable process; where special-cause variation is present the indices describe a moving target, and a control chart should settle the process first.",
            n_unstable, stability_unit, if (n_unstable == 1) "" else "s")
  }

Step 9: Verdict wording

capability_word <- if (cpk >= 1.33) "capable" else if (cpk >= 1.0) "marginal" else "not capable"
  verdict <- sprintf("%s(Cpk %s)", tools::toTitleCase(capability_word), fmt_idx(cpk, 2))

  metrics <- list()
  metrics[["Measurements"]]        <- n
  metrics[["Process Mean"]]        <- round(mu, 4)
  metrics[["Sigma(Within)"]]      <- round(sigma_within, 4)
  metrics[["Sigma(Overall)"]]     <- round(sd_overall, 4)
  if (is.finite(cp)) metrics[["Cp"]]  <- round(cp, 3)
  metrics[["Cpk"]]                 <- round(cpk, 3)
  if (is.finite(pp)) metrics[["Pp"]]  <- round(pp, 3)
  metrics[["Ppk"]]                 <- round(ppk, 3)
  metrics[["Expected PPM"]]        <- round(ppm_exp_lt, 2)
  metrics[["Observed PPM"]]        <- round(ppm_obs, 1)
  metrics[["Process Sigma Level"]] <- round(sigma_level, 2)
  metrics[["Normality"]]           <- if (normal_ok) "not rejected" else "rejected"
  metrics[["Verdict"]]             <- verdict

  spec_phrase <- if (spec_sides == "two-sided") {
    sprintf("specification %s to %s", fmt_num(lsl, 4), fmt_num(usl, 4))
  } else if (spec_sides == "upper only") {
    sprintf("upper specification limit %s", fmt_num(usl, 4))
  } else {
    sprintf("lower specification limit %s", fmt_num(lsl, 4))
  }

  json_output <- list(
    answer = paste0(
      "Capability of ", measurement_name, " against the ", spec_phrase,
      " across ", format(n, big.mark = ","), " ", units_word(n), ": Cpk = ",
      fmt_idx(cpk, 2), " (95 percent interval ", fmt_idx(cpk_ci_low, 2), " to ",
      fmt_idx(cpk_ci_high, 2), "), Ppk = ", fmt_idx(ppk, 2),
      if (is.finite(cp)) paste0(", Cp = ", fmt_idx(cp, 2)) else "",
      " — the process is ", capability_word,
      " against the usual 1.33 threshold. The normal model puts the long-term defect rate at ",
      fmt_ppm(ppm_exp_lt), " parts per million(process sigma ",
      fmt_idx(sigma_level, 2), " with the 1.5 shift convention), against ",
      fmt_ppm(ppm_obs), " parts per million actually observed. ",
      if (normal_ok) {
        "Normality is not rejected, so those model-based numbers stand."
      } else {
        "Normality is rejected on this data, so the indices and the model-based defect rate should not be trusted as stated."
      }
    ),
    cards = lapply(
      c("tldr", "overview", "preprocessing", "capability_histogram",
        "capability_indices", "defect_estimate", "normality_check",
        "variation_sources"),
      function(cid) list(id = cid, metrics = metrics)
    )
  )

  list(
    initial_rows = initial_rows, final_rows = final_rows, rows_removed = rows_removed,
    n = n, n_dropped_na = n_dropped_na, n_distinct = n_distinct,
    measurement_name = measurement_name, subgroup_name = subgroup_name,
    lsl = lsl, usl = usl, target = target, has_lsl = has_lsl, has_usl = has_usl,
    spec_sides = spec_sides, spec_phrase = spec_phrase,
    lsl_source = lsl_r$source, usl_source = usl_r$source, target_source = tgt_r$source,
    mu = mu, sd_overall = sd_overall, sigma_within = sigma_within,
    sigma_source = sigma_source,
    cp = cp, cpu = cpu, cpl = cpl, cpk = cpk, pp = pp, ppu = ppu, ppl = ppl,
    ppk = ppk, cpm = cpm, k = k, binding_side = binding_side,
    cpk_ci_low = cpk_ci_low, cpk_ci_high = cpk_ci_high,
    n_below = n_below, n_above = n_above, n_out = n_out,
    ppm_obs = ppm_obs, ppm_obs_below = ppm_obs_below, ppm_obs_above = ppm_obs_above,
    ppm_exp_st = ppm_exp_st, ppm_exp_lt = ppm_exp_lt,
    ppm_exp_lt_below = ppm_exp_lt_below, ppm_exp_lt_above = ppm_exp_lt_above,
    z_bench_st = z_bench_st, z_bench_lt = z_bench_lt, sigma_level = sigma_level,
    yield_obs = yield_obs,
    shapiro_p = shapiro_p, ad_p = ad_p, ad_stat = ad$A2,
    skewness = skewness, kurtosis = kurtosis, normal_ok = normal_ok,
    normality_text = normality_text,
    n_groups = n_groups, sigma_between = sigma_between, pct_between = pct_between,
    anova_f = anova_f, anova_p = anova_p,
    n_unstable = n_unstable, stability_text = stability_text,
    gauge_res = gauge_res, gauge_coarse = gauge_coarse,
    capability_word = capability_word, verdict = verdict,
    dist_df = dist_df, qq_df = qq_df, indices_df = indices_df,
    defects_df = defects_df, variation_df = variation_df,
    metrics = metrics, json_output = json_output
  )
}
Your data has more stories to tell.Run any analysis on your own data — validated R modules, interactive reports, AI insights, and PDF export. 500 free credits on signup.
Try Free — No SignupSign Up Free

Cite this analysis

Report an Issue

Tell us what's wrong. You'll get a free re-run of this analysis so you can try again with different parameters. If the re-run still doesn't meet your expectations, we'll refund your credits.

Want to run this analysis on your own data? Upload CSV — Free Analysis See Pricing