Executive Summary
Whether diameter mm is in statistical control
The production process is out of statistical control: 2 special-cause signals were detected among 75 measurements of diameter mm. The process mean is 5.614 mm with natural control limits of 5.475 to 5.753 mm (three sigma = 0.046 mm). Although 97.3% of points remain in control, the presence of any flagged signals indicates the process has shifted or spiked and warrants investigation to identify and eliminate the assignable cause.
Analysis Overview
An Individuals control chart of diameter mm over 75 ordered points.
A control chart distinguishes common-cause variation—the ordinary noise in a stable process—from special-cause variation, which signals that something changed. The center line (5.614 mm) and 3-sigma control limits (5.475 to 5.753 mm) describe the natural process behavior, estimated from the moving range between consecutive measurements. These limits are not specification targets but the voice of the process itself. A process in statistical control is stable and predictable, though not necessarily meeting a customer spec. Control status tells you whether the process is behaving consistently, not whether it is good enough.
Data Quality
How the raw rows became an ordered measurement series.
All 75 rows loaded with no missing measurements. The points were ordered by unit number (1 to 75) in numeric sequence, preserving the production order. No rows were dropped. The final series contains 75 ordered measurements of diameter mm, ready for control charting.
Individuals Control Chart
Diameter Mm in order against the center line and 3-sigma limits.
Diameter measurements cluster tightly around the center line of 5.614 mm across the 75-unit sequence, with no excursions beyond the 3-sigma limits of 5.475 to 5.753 mm. The scatter is consistent and symmetric, showing typical common-cause noise. Two points—units 55 and 57—stand out as flagged by Rule 5 (near-limit clustering on the same side), signaling a localized process shift worth investigating. The rest of the sequence shows no sustained drift, trend, or run pattern.
Special-Cause Signals
Each Western Electric / Nelson rule with the points it flagged.
| Rule | Description | Points Flagged | Count |
|---|---|---|---|
| Rule 1 — beyond limits | A single point beyond the 3-sigma control limits | none | 0 |
| Rule 2 — sustained shift | Nine points in a row on the same side of the center line | none | 0 |
| Rule 3 — trend | Six points in a row steadily increasing or decreasing | none | 0 |
| Rule 5 — near-limit cluster | Two of three points in a row beyond 2 sigma on the same side | 55, 57 | 2 |
Rule 5 (near-limit cluster: two of three points beyond 2-sigma on the same side) flagged 2 points: units 55 and 57. No points violated Rule 1 (beyond 3-sigma limits), Rule 2 (nine consecutive points on one side of center), or Rule 3 (six consecutive points in a trend). The near-limit cluster is the sole special-cause signal and the reason the process is classified out of control.
Process Summary
The headline process statistics and the in-control verdict.
| Statistic | Value | Interpretation |
|---|---|---|
| Process mean (center line) | 5.614 | The average of diameter mm over the ordered points |
| Estimated sigma | 0.046 | Within-process standard deviation, estimated as MRbar divided by 1.128 |
| Upper control limit (UCL) | 5.753 | Center line plus three sigma — the upper natural process limit |
| Lower control limit (LCL) | 5.475 | Center line minus three sigma — the lower natural process limit |
| Mean moving range (MRbar) | 0.052 | Average absolute change between consecutive measurements |
| Points in control | 97.3% | 73 of 75 points show no special-cause signal |
| Verdict | Out of statistical control | 2 special-cause signals across Rule 5 |
The process mean is 5.614 mm with an estimated sigma of 0.046 mm (derived from a mean moving range of 0.052). The upper and lower control limits sit at 5.753 and 5.475 mm respectively. Of the 75 points, 97.3% remain in control; 2 points triggered special-cause signals. The verdict is out of statistical control, driven entirely by Rule 5. These limits reflect what the process naturally produces, not what the customer requires—a stable, predictable process can still fail to meet a specification.
Control Chart — Is Your Process In Control?
Builds an Individuals (I-MR) control chart from an ordered sequence of measurements: orders the points, sets the center line at the process mean, estimates sigma from the average moving range, draws the 3-sigma natural process limits, and runs a practical subset of the Western Electric / Nelson rules to flag special-cause signals — then decides whether the process is in statistical control.
Why This Method?
The Individuals chart is the SPC / Six Sigma workhorse for a stream of one-value-per-point measurements. Estimating sigma from the moving range (rather than the overall standard deviation) keeps the limits honest when the process shifts, so a sustained change shows up as points beyond the limits instead of quietly inflating the spread. Base R only.
What This Analysis Covers
- Individuals control chart with center line and 3-sigma limits
- Western Electric / Nelson special-cause signals, flagged point by point
- A process summary with the in-control verdict
Standard Library
Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {sequence, measurement}. 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))Core Analysis Pipeline
Step 1: Order by the sequence column
Try dates first (forecasting-style parser); else a numeric sort; else fall back to the input order as given.
raw_seq <- df$sequence
non_blank <- !(is.na(raw_seq) | !nzchar(trimws(as.character(raw_seq))))
n_nonblank <- sum(non_blank)
d <- parse_dates_robust(raw_seq)
frac_dates <- if (n_nonblank > 0) sum(!is.na(d)) / n_nonblank else 0
numseq <- suppressWarnings(as.numeric(as.character(raw_seq)))
frac_num <- if (n_nonblank > 0) sum(!is.na(numseq)) / n_nonblank else 0
if (frac_dates >= 0.8) {
ord <- order(d, na.last = TRUE)
seq_kind <- "date"
} else if (frac_num >= 0.95 && n_nonblank > 0) {
ord <- order(numseq, na.last = TRUE)
seq_kind <- "numeric"
} else {
ord <- seq_len(nrow(df))
seq_kind <- "order"
}
df <- df[ord, , drop = FALSE]Step 2: Coerce the measurement (95% rule) and drop missing values
mv <- df$measurement
if (!is.numeric(mv)) {
conv <- suppressWarnings(as.numeric(as.character(mv)))
n_orig <- sum(!is.na(mv) & nzchar(as.character(mv)))
if (n_orig > 0 && sum(!is.na(conv)) >= 0.95 * n_orig) {
mv <- conv
} else {
stop(sprintf("The '%s' column is not numeric — a control chart needs a numeric measurement.",
measurement_name))
}
}
mv <- as.numeric(mv)
d_sorted <- d[ord]
num_sorted <- numseq[ord]
keep <- !is.na(mv)
n_dropped_na <- sum(!keep)
x <- mv[keep]
d_keep <- d_sorted[keep]
num_keep <- num_sorted[keep]
n_points <- length(x)
if (n_points < 10) {
stop(sprintf("Only %d valid %s of '%s' — a control chart needs at least 10 ordered measurements.",
n_points, pts(n_points), measurement_name))
}
final_rows <- n_points
rows_removed <- initial_rows - final_rowsStep 3: Sequence range description (for the prose)
seq_range_text <- if (seq_kind == "date" && any(!is.na(d_keep))) {
sprintf("%s to %s", format(min(d_keep, na.rm = TRUE)),
format(max(d_keep, na.rm = TRUE)))
} else if (seq_kind == "numeric" && any(!is.na(num_keep))) {
sprintf("%s to %s", fmt_num(min(num_keep, na.rm = TRUE)),
fmt_num(max(num_keep, na.rm = TRUE)))
} else {
"the input order"
}Step 4: Individuals chart geometry (center + moving-range sigma)
cl <- mean(x)
mr <- abs(diff(x))
mrbar <- mean(mr)
sigma <- mrbar / 1.128 # d2 for a moving range of 2
if (!is.finite(sigma) || sigma <= 0) {
sigma <- 0
ucl <- cl; lcl <- cl; two_up <- cl; two_dn <- cl
} else {
ucl <- cl + 3 * sigma; lcl <- cl - 3 * sigma
two_up <- cl + 2 * sigma; two_dn <- cl - 2 * sigma
}
mr_ucl <- 3.267 * mrbar # MR-chart upper limit (MR lower limit = 0)Step 5: Western Electric / Nelson rules (a practical subset)
if (sigma > 0) {
flag1 <- (x > ucl) | (x < lcl) # beyond 3 sigma
flag2 <- flag_same_side_run(x, cl, 9) # nine same side
flag3 <- flag_trend_run(x, 6) # six trending
flag5 <- flag_two_of_three(x, two_up, two_dn) # 2 of 3 beyond 2 sigma
} else {
flag1 <- flag2 <- flag3 <- flag5 <- rep(FALSE, n_points)
}
flag_any <- flag1 | flag2 | flag3 | flag5
rule_defs <- list(
list(rule = "Rule 1 — beyond limits",
description = "A single point beyond the 3-sigma control limits", flag = flag1),
list(rule = "Rule 2 — sustained shift",
description = "Nine points in a row on the same side of the center line", flag = flag2),
list(rule = "Rule 3 — trend",
description = "Six points in a row steadily increasing or decreasing", flag = flag3),
list(rule = "Rule 5 — near-limit cluster",
description = "Two of three points in a row beyond 2 sigma on the same side", flag = flag5)
)
rule_counts <- setNames(vapply(rule_defs, function(r) sum(r$flag), integer(1)),
c("rule1", "rule2", "rule3", "rule5"))
n_flagged <- sum(flag_any)
n_in_control <- n_points - n_flagged
pct_in_control <- 100 * n_in_control / n_points
in_control <- (n_flagged == 0)
verdict <- if (in_control) "In statistical control" else "Out of statistical control"
rules_fired <- vapply(rule_defs, function(r) sum(r$flag) > 0, logical(1))
rules_fired_text <- if (!any(rules_fired)) {
"no rules"
} else {
paste(sub(" —.*$", "", vapply(rule_defs[rules_fired], function(r) r$rule, character(1))),
collapse = ", ")
}Step 6: Control-points dataset (<=2000, always keep every flagged point)
if (n_points <= 2000) {
cp_idx <- seq_len(n_points)
} else {
flagged_idx <- which(flag_any)
norm_idx <- setdiff(seq_len(n_points), flagged_idx)
budget <- max(0, 2000 - length(flagged_idx))
set.seed(42)
if (length(norm_idx) > budget) norm_idx <- sample(norm_idx, budget)
cp_idx <- sort(c(flagged_idx, norm_idx))
}
control_points_df <- data.frame(
sequence_index = cp_idx,
measurement_value = round(x[cp_idx], 4),
status = ifelse(flag_any[cp_idx], "out of control", "in control"),
stringsAsFactors = FALSE
)
rownames(control_points_df) <- NULLStep 7: Rule-violations table
fmt_points <- function(idx) {
if (length(idx) == 0) return("none")
shown <- head(idx, 12)
txt <- paste(shown, collapse = ", ")
if (length(idx) > 12) txt <- paste0(txt, ", and ", length(idx) - 12, " more")
txt
}
if (n_flagged == 0) {
violations_df <- data.frame(
rule = "No special-cause signals",
description = "Every point sits within the control limits with no shift, trend, or near-limit cluster — the process appears stable",
points_flagged = "none",
count = 0L,
stringsAsFactors = FALSE
)
} else {
violations_df <- do.call(rbind, lapply(rule_defs, function(r) {
idx <- which(r$flag)
data.frame(rule = r$rule, description = r$description,
points_flagged = fmt_points(idx), count = length(idx),
stringsAsFactors = FALSE)
}))
}
rownames(violations_df) <- NULLStep 8: Process-summary table
process_stats_df <- data.frame(
statistic = c("Process mean(center line)", "Estimated sigma",
"Upper control limit(UCL)", "Lower control limit(LCL)",
"Mean moving range(MRbar)", "Points in control",
"Verdict"),
value = c(fmt_num(cl, 3), fmt_num(sigma, 3), fmt_num(ucl, 3), fmt_num(lcl, 3),
fmt_num(mrbar, 3), paste0(fmt_num(pct_in_control, 1), "%"),
verdict),
interpretation = c(
sprintf("The average of %s over the ordered points", measurement_name),
"Within-process standard deviation, estimated as MRbar divided by 1.128",
"Center line plus three sigma — the upper natural process limit",
"Center line minus three sigma — the lower natural process limit",
"Average absolute change between consecutive measurements",
sprintf("%d of %d %s show no special-cause signal", n_in_control, n_points, pts(n_points)),
if (in_control) "No rule fired — the process is stable and predictable"
else sprintf("%d special-cause %s across %s", n_flagged, sig_word(n_flagged), rules_fired_text)
),
stringsAsFactors = FALSE
)
rownames(process_stats_df) <- NULL
metrics <- list(
`Points` = n_points,
`Process Mean` = round(cl, 3),
`Estimated Sigma` = round(sigma, 3),
`UCL` = round(ucl, 3),
`LCL` = round(lcl, 3),
`Special-Cause Signals` = as.integer(n_flagged),
`Points In Control(%)` = round(pct_in_control, 1),
`Verdict` = if (in_control) "In control" else "Out of control"
)
json_output <- list(
answer = paste0(
"Individuals control chart on ", n_points, " ordered ",
measurement_name, " ", pts(n_points), " (mean ", fmt_num(cl, 2),
", sigma ", fmt_num(sigma, 3), ", natural limits ", fmt_num(lcl, 2),
" to ", fmt_num(ucl, 2), "): ",
if (in_control) {
paste0("no special-cause signals — the process is in statistical control.")
} else {
paste0(n_flagged, " out-of-control ", pts(n_flagged), " flagged by ",
rules_fired_text, " — the process is out of statistical control.")
}
),
cards = lapply(
c("tldr", "overview", "preprocessing", "control_chart",
"rule_violations", "process_summary"),
function(cid) list(id = cid, metrics = metrics)
)
)
list(
initial_rows = initial_rows, final_rows = final_rows, rows_removed = rows_removed,
sequence_name = sequence_name, measurement_name = measurement_name,
seq_kind = seq_kind, seq_range_text = seq_range_text,
n_points = n_points, n_dropped_na = n_dropped_na,
cl = cl, sigma = sigma, mrbar = mrbar, ucl = ucl, lcl = lcl,
two_up = two_up, two_dn = two_dn, mr_ucl = mr_ucl,
rule_counts = rule_counts, n_flagged = n_flagged, n_in_control = n_in_control,
pct_in_control = pct_in_control, in_control = in_control, verdict = verdict,
rules_fired_text = rules_fired_text,
control_points_df = control_points_df, violations_df = violations_df,
process_stats_df = process_stats_df,
metrics = metrics, json_output = json_output
)
}