Executive Summary
Whether Fill Weight g is in statistical control
Your fill process is out of statistical control. The process mean is 101.575 g with natural limits of 96.356 to 106.794 g. Across the 80 measurements, 53 points triggered special-cause signals—9 points beyond the 3-sigma limits (Rule 1), 51 points in sustained same-side runs (Rule 2), and 18 points in near-limit clusters (Rule 5). Only 33.8% of points remain in control. This pattern indicates the presence of assignable causes that have shifted or disrupted the process at multiple points in the sequence.
Analysis Overview
An Individuals control chart of Fill Weight g over 80 ordered points.
A control chart distinguishes common-cause variation (ordinary process noise) from special-cause variation (signals of change). Your fill process has a mean of 101.575 g and natural process limits of 96.356 to 106.794 g (three sigma, where sigma = 1.74). These limits describe what your process actually does, not what you want it to do—specification limits are separate. A process in statistical control is stable and predictable, though not necessarily meeting target. Critically, being in control does not mean being good; it means the variation is understood and repeatable. Your fill process is currently not in control.
Data Quality
How the raw rows became an ordered measurement series.
80 rows loaded. The points were ordered by the numeric value of Sample Number (1 to 80). No measurements were missing. Final series: 80 ordered points of Fill Weight g.
Individuals Control Chart
Fill Weight g in order against the center line and 3-sigma limits.
The individuals chart shows Fill Weight g plotted in sequence against the center line (101.575 g), upper limit (106.794 g), and lower limit (96.356 g). Of the 80 points, 53 are flagged as out of control. The early sequence (samples 1–11) shows a sustained cluster below the mean, triggering Rule 2 (nine points on one side). Points 33, 61, 65, 70, 71, 72, 73, 77, and 80 breach the 3-sigma limits themselves (Rule 1). A near-limit cluster (Rule 5) appears around samples 30–33, 38, 40, and 61–73, indicating measurements bunching against the statistical boundaries. The pattern is not a smooth trend but rather a series of runs and excursions, suggesting intermittent process shifts rather than a single persistent drift.
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 | 33, 61, 65, 70, 71, 72, 73, 77, 80 | 9 |
| Rule 2 — sustained shift | Nine points in a row on the same side of the center line | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 26, and 39 more | 51 |
| 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 | 30, 32, 33, 38, 40, 61, 62, 65, 67, 68, 69, 70, and 6 more | 18 |
Three control rules fired across 53 flagged points. Rule 1 (beyond limits) caught 9 points: 33, 61, 65, 70, 71, 72, 73, 77, 80. Rule 2 (sustained shift—nine consecutive points on one side of center) fired 51 times, including the opening run (1–11) and additional runs at point 26 and point 39 onward. Rule 3 (six-point trend) did not fire. Rule 5 (near-limit cluster—two of three points beyond 2-sigma on the same side) flagged 18 points: 30, 32, 33, 38, 40, 61, 62, 65, 67, 68, 69, 70, and 6 others. The dominance of Rule 2 and Rule 5 signals indicates the process oscillates between sustained runs and near-boundary clustering rather than drifting monotonically.
Process Summary
The headline process statistics and the in-control verdict.
| Statistic | Value | Interpretation |
|---|---|---|
| Process mean (center line) | 101.575 | The average of Fill Weight g over the ordered points |
| Estimated sigma | 1.74 | Within-process standard deviation, estimated as MRbar divided by 1.128 |
| Upper control limit (UCL) | 106.794 | Center line plus three sigma — the upper natural process limit |
| Lower control limit (LCL) | 96.356 | Center line minus three sigma — the lower natural process limit |
| Mean moving range (MRbar) | 1.962 | Average absolute change between consecutive measurements |
| Points in control | 33.8% | 27 of 80 points show no special-cause signal |
| Verdict | Out of statistical control | 53 special-cause signals across Rule 1, Rule 2, Rule 5 |
Your fill process has a mean of 101.575 g and estimated sigma of 1.74 g (calculated from a mean moving range of 1.962 g). The natural process limits are 96.356 g (lower) and 106.794 g (upper). Only 27 of 80 points (33.8%) show no special-cause signal; 53 are flagged. The verdict is out of statistical control. These statistics describe the voice of the process—what it currently does—not a specification target. An in-control process is predictable and repeatable, which supports root-cause investigation and improvement; your process is neither, signaling the need to identify and eliminate the assignable causes driving the 53 violations.
Methodology
Statistical methodology and diagnostics for Control Chart — Is Your Process In Control?
Statistical Method
Standard-library analysis: an Individuals (I-MR) control chart for a sequence of measurements — the SPC / Six Sigma staple for telling a stable process apart from one throwing special-cause signals. Computes the center line, the 3-sigma natural process limits from the moving range, and runs a practical subset of the Western Electric / Nelson rules to flag out-of-control points. Works on any dataset: map a sequence column (date, batch, or sample order) and the measured value.
- Measurements are individual values recorded in a meaningful order (one per point)
- The measured column is numeric or cleanly convertible
- Consecutive points are comparable — the moving range estimates the natural process spread
- Control limits describe the process as it is (voice of the process), not what you want it to be (voice of the customer) — no spec limits or Cp / Cpk are computed
- The Individuals chart is less sensitive to small shifts than subgrouped charts and can be affected by strong autocorrelation
- In statistical control means predictable, not good — a stable process can still miss its target
Analysis Code
Complete R source code for this analysis
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
)
}