Standard Charts
Executive Summary

Executive Summary

Whether diameter mm is in statistical control

Points
75
Process Mean
5.614
Estimated Sigma
0.046
UCL
5.753
LCL
5.475
Special-Cause Signals
2
Points In Control (%)
97.3
Verdict
Out of control
Charting diameter mm as an Individuals control chart. The process mean is 5.61, with natural process limits of 5.47 to 5.75 (three sigma, sigma = 0.046). 2 out-of-control points were flagged by Rule 5, so the process is out of statistical control. 97.3% of points remain in control.
What this means

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.

Overview

Analysis Overview

An Individuals control chart of diameter mm over 75 ordered points.

N Points75
Process Mean5.614
Estimated Sigma0.046
In ControlFalse
What this means

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 Preparation

Data Quality

How the raw rows became an ordered measurement series.

Initial Rows75
Final Rows75
N Dropped0
Sequence Ordernumeric
What this means

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.

Visualization

Individuals Control Chart

Diameter Mm in order against the center line and 3-sigma limits.

What this means

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.

Data Table

Special-Cause Signals

Each Western Electric / Nelson rule with the points it flagged.

RuleDescriptionPoints FlaggedCount
Rule 1 — beyond limitsA single point beyond the 3-sigma control limitsnone0
Rule 2 — sustained shiftNine points in a row on the same side of the center linenone0
Rule 3 — trendSix points in a row steadily increasing or decreasingnone0
Rule 5 — near-limit clusterTwo of three points in a row beyond 2 sigma on the same side55, 572
What this means

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.

Data Table

Process Summary

The headline process statistics and the in-control verdict.

StatisticValueInterpretation
Process mean (center line)5.614The average of diameter mm over the ordered points
Estimated sigma0.046Within-process standard deviation, estimated as MRbar divided by 1.128
Upper control limit (UCL)5.753Center line plus three sigma — the upper natural process limit
Lower control limit (LCL)5.475Center line minus three sigma — the lower natural process limit
Mean moving range (MRbar)0.052Average absolute change between consecutive measurements
Points in control97.3%73 of 75 points show no special-cause signal
VerdictOut of statistical control2 special-cause signals across Rule 5
What this means

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.

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

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 &#x27;%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 &#x27;%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_rows

Step 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) <- NULL

Step 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) <- NULL

Step 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
  )
}
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