10  Quality Control

This chapter demonstrates how to use ivdtools to monitor continuous quality-control results. Current functionality includes listing implemented Westgard rules, creating a Levey–Jennings chart and locating rule violations, and using a Youden plot to inspect two QC levels jointly.

ImportantScope of Use

The routine QC, lot-change labels, and Youden data in this chapter are deterministic teaching data. They are not original standard data from CLSI EP23, EP26, or EP33 and are not intended to establish general acceptance criteria.

qc_chart() analyzes one numeric sequence at a time. Rules are scanned in the current row order of the data frame; run supplies only the x-axis for the plot and does not sort the data. group is used only for faceting and does not reset continuous rules between groups. The current severity labels mark only 1-3s as out_of_control; all other rule violations are labeled warning. Thus n_warn and n_oc are software display categories and are not equivalent to a laboratory multirule release strategy.

The statistical unit and workflow for QC analysis are:

%%{init: {"theme":"base","themeVariables":{"primaryColor":"#EAF2FB","primaryBorderColor":"#2780E3","primaryTextColor":"#212529","lineColor":"#6C757D","secondaryColor":"#EAF7E6","tertiaryColor":"#FFF4E8","fontSize":"13px"},"flowchart":{"nodeSpacing":18,"rankSpacing":24,"padding":6}}}%%

flowchart LR
    A["Approved targets, SD, and rules"] --> B["Order single-level QC by actual run sequence"]
    B --> C["qc_chart()"]
    C --> D["Violation positions by rule"]
    C --> E["Levey--Jennings chart"]
    F["Two QC levels from the same run"] --> G["youden_plot()"]
    G --> H["Joint shift and ±2SD rectangle"]
    D --> I["Investigation, correction, verification, and release record"]
    E --> I
    H --> I

    classDef input fill:#EAF2FB,stroke:#2780E3,color:#212529
    classDef process fill:#EAF7E6,stroke:#3FB618,color:#212529
    classDef output fill:#F4EEFB,stroke:#9954BB,color:#212529
    class A,F input
    class B,C,G process
    class D,E,H,I output

Code
library(ivdtools)
library(readr)

10.1 Main Functions

Function Main purpose Main results
list_westgard() View rule names recognized by qc_chart() Named vector of rule descriptions
qc_chart() Standardize one QC sequence, scan rules, and create a Levey–Jennings chart Targets, SD, positions by rule, unique violation points, and plotting data
print() Print a summary of a QC or Youden object Targets, sample size, rules, or outside-rectangle positions
plot() Plot a result object Levey–Jennings or Youden plot
youden_plot() Jointly inspect two QC levels from the same run Pearson correlation, positions outside the ±2SD rectangle, and plotting data

10.1.1 View Rules with list_westgard()

Usage

Code
list_westgard()

Westgard QC Rules
  -------------------------------------------------- 
  1-2s    1 point exceeds +/-2s (warning)
  1-3s    1 point exceeds +/-3s (out of control)
  2-2s    2 consecutive points exceed +/-2s (same side)
  R-4s    2 consecutive points differ by at least 4s
  4-1s    4 consecutive points exceed +/-1s (same side)
  8x      8 consecutive points on same side of mean
  10x     10 consecutive points on same side of mean
  -------------------------------------------------- 

Exact definitions in the current source code

Let the target mean be \(μ_0\) and the target standard deviation be \(σ_0>0\). The standardized deviation of result \(i\) is

\[ z_i=\frac{x_i-μ_0}{σ_0}. \]

Rule ivdtools condition Points marked
1-2s \(|z_i|>2\) Current point
1-3s \(|z_i|>3\) Current point
2-2s Both adjacent points are \(>2\), or both are \(<-2\) The two points in the adjacent window
R-4s Each adjacent point has \(|z|\ge2\) and \(|z_i-z_{i+1}|\ge4\) The two points in the adjacent window
4-1s Four consecutive points are all \(>1\), or all \(<-1\) The four points in the window
8x Eight consecutive points are all \(>0\), or all \(<0\) The eight points in the window
10x Ten consecutive points are all \(>0\), or all \(<0\) The ten points in the window

Interpret inequality boundaries exactly as shown: 1-2s, 1-3s, 2-2s, and 4-1s use strict “greater than”; a point exactly at \(2s\), $3s, or $1s does not trigger the rule. The single-point threshold and two-point range for R-4s use “greater than or equal to.” A point may occur in several rules; n_violations counts unique positions across all rules.

WarningBoundary of Multilevel Westgard Rules

Classical multirule schemes can combine rules across QC levels in the same run or across consecutive runs at the same level. The current qc_chart() scans only the vector supplied to it; R-4s and the other window rules are implemented over adjacent rows, and the function does not automatically recognize two levels from the same run. Multilevel data must not simply be stacked into one column and treated as a standard multilevel Westgard decision. youden_plot() can help inspect two levels, but it does not apply cross-level Westgard rejection rules.

10.1.2 Single-Sequence Control Chart with qc_chart()

Usage

Code
qc_chart(
  data,
  value,
  rules = "1-2s,1-3s,2-2s,R-4s,4-1s,10x",
  mean = NULL,
  sd = NULL,
  group = NULL,
  run = NULL
)

print(x)
plot(x)

Arguments

Argument Description
data Data frame; each row is one QC result in the sequence.
value Name of the numeric QC result column.
rules Comma-separated string; only names listed by list_westgard() are allowed. 8x is not included by default.
mean Target mean; if NULL, estimated from the current complete data. Routine monitoring should normally pass an approved target explicitly.
sd Positive target SD; if NULL, estimated from the current complete data.
group Optional grouping column used only for plot facets; targets are not estimated separately and rule windows are not reset.
run Optional x-axis column; if NULL, uses \(1,\ldots,n\). It does not change scan order.

If different control materials, lots, or instruments need their own targets and independent rule resets, first split the data into a named list with split_by_factors(data, variable="..."), then call qc_chart() separately for each data frame. A group facet cannot replace independent analyses.

Calculations and returned values

Rows with missing value are removed before calculation; corresponding complete rows of run and group are retained. If targets are not supplied, the sample mean and sample SD of all complete results are used:

\[ \hat\mu=\frac1n\sum_i x_i,\qquad \hat\sigma=\sqrt{\frac{\sum_i(x_i-\hat\mu)^2}{n-1}}. \]

Estimating targets from the same monitoring sequence can absorb an ongoing shift and widen the control limits. It is therefore suitable only for exploratory or establishment phases and should not be re-estimated on a rolling basis for each routine decision.

Main returned fields are:

Field Meaning
target_mean, target_sd Targets actually used for standardization and control limits
rule_vec Rules actually scanned and their order
violations Named list; each element contains positions in the sequence after missing rows are removed
n_violations Number of unique positions hit by any rule
n_warn Number of unique points hit but not by 1-3s
n_oc Number of unique points hit by 1-3s
plot_data Complete values, x-axis, group, displayed severity, and rule labels
n_total, n_complete, n_miss Original, complete, and missing record counts

plot() draws the target mean and the \(±1s\), \(±2s\), and \(±3s\) lines and labels points hit by rules. Positions in violations refer to the cleaned sequence and may not equal original data-frame row numbers; when missing values occur, trace records through plot_data$run or the original run identifier.

10.1.3 Two-Level Joint Plot with youden_plot()

Usage

Code
youden_plot(
  data,
  sample1,
  sample2,
  mean1 = NULL,
  mean2 = NULL,
  sd1 = NULL,
  sd2 = NULL
)

print(x)
plot(x)

Arguments

Argument Description
data Wide-format data frame; each row must represent two levels from the same run.
sample1, sample2 Names of the two numeric QC-level columns.
mean1, mean2 Target means; if omitted, estimated from complete paired data.
sd1, sd2 Positive target SDs; if omitted, estimated from complete paired data.

Only rows with both levels nonmissing are retained. Standardized coordinates are

\[ z_{1i}=\frac{x_{1i}-\mu_1}{\sigma_1},\qquad z_{2i}=\frac{x_{2i}-\mu_2}{\sigma_2}. \]

The source code defines a point as inside the rectangle when

\[ \lvert z_{1i}\rvert\le2 \quad\text{and}\quad \lvert z_{2i}\rvert\le2. \]

If either level strictly exceeds \(±2s\), the point is included in outside_rows. Boundary points remain inside. The object also calculates the Pearson correlation between the two original result columns:

\[ r=\frac{\sum_i(x_{1i}-\bar x_1)(x_{2i}-\bar x_2)} {\sqrt{\sum_i(x_{1i}-\bar x_1)^2\sum_i(x_{2i}-\bar x_2)^2}}. \]

Main fields include mean1, mean2, sd1, sd2, correlation, n_outside, outside_rows, and plot_data. outside_rows contains original data-frame row numbers, unlike the cleaned positions returned by qc_chart().

The current plot displays both levels on their original measurement scales and overlays \(±1s\) and \(±2s\) rectangles. To determine whether the two levels shifted in the same direction, compare the directions of \((z_1,z_2)\) relative to the center. Do not interpret an original-scale \(y=x\) reference line, with different targets or units, as meaning that the two levels should have equal numerical values. correlation and the outside-rectangle flag are descriptive results; there is no error ellipse, joint probability test, or automatic out-of-control conclusion.

Return value

youden_plot() returns two-level coordinates, ellipse/limit data, and outlier classifications, and invisibly returns a ggplot object.

10.2 Analysis Examples

10.2.1 Routine QC with Known Targets

Read 30 teaching results ordered by run:

Code
qc_daily <- read_csv(
  "./data/010-质量控制/qc-daily.csv",
  show_col_types = FALSE
)
qc_daily <- as.data.frame(qc_daily)
dim(qc_daily)
[1] 30  2
Code
head(qc_daily)
  run  value
1   1 103.24
2   2 103.06
3   3  94.52
4   4  99.51
5   5  99.14
6   6  95.62

The protocol specifies a target mean of 100 and target SD of 3, so pass them explicitly rather than re-estimating them from the monitoring data:

Code
qc_daily_fit <- qc_chart(
  qc_daily,
  value = "value",
  rules = "1-2s,1-3s,2-2s,R-4s,4-1s,10x",
  mean = 100,
  sd = 3,
  run = "run"
)
print(qc_daily_fit)

QC Control Chart
  Value column:    value
  Mean (target):   100.0000
  SD (target):     3.0000
  N (total):       30  |  Complete: 30  |  Missing: 0
  Rules applied:   1-2s, 1-3s, 2-2s, R-4s, 4-1s, 10x

  Violations:
  -------------------------------------------------- 
    1-2s  : points 15, 23
    1-3s  : points 15
  -> 2 violation(s): 1 warning(s), 1 out of control

Point 15 meets both 1-2s and 1-3s and is counted as one out_of_control point; point 23 meets only 1-2s and is counted as one warning point. Therefore n_violations=2, n_warn=1, and n_oc=1.

Code
plot(qc_daily_fit)

10.2.1.1 Known Targets versus In-Data Estimates

If mean and sd are omitted, the function re-estimates the center and dispersion from these 30 results:

Code
qc_daily_estimated <- qc_chart(
  qc_daily,
  value = "value",
  run = "run"
)
qc_daily_estimated

QC Control Chart
  Value column:    value
  Mean (target):   100.2890
  SD (target):     3.2314
  N (total):       30  |  Complete: 30  |  Missing: 0
  Rules applied:   1-2s, 1-3s, 2-2s, R-4s, 4-1s, 10x

  Violations:
  -------------------------------------------------- 
    1-2s  : points 15, 23
  -> 2 violation(s): 2 warning(s), 0 out of control

The in-data estimates are a mean of 100.2890 and an SD of 3.2314. Point 15 no longer strictly exceeds the new \(+3s\) limit, so its display classification changes from out_of_control to warning. This is why routine monitoring should not update targets and SDs on a rolling basis using the current lot’s data.

10.2.2 Continuous Display with Lot-Change Labels

The first, middle, and last 10 points of this teaching data are labeled L1, L2, and L3, respectively; L2 has an artificial overall shift:

Code
qc_lot <- read_csv(
  "./data/010-质量控制/qc-lot-change.csv",
  show_col_types = FALSE
)
qc_lot <- as.data.frame(qc_lot)
Code
qc_lot_fit <- qc_chart(
  qc_lot,
  value = "value",
  mean = 100,
  sd = 3,
  run = "run",
  group = "lot"
)
print(qc_lot_fit)

QC Control Chart
  Value column:    value
  Mean (target):   100.0000
  SD (target):     3.0000
  N (total):       30  |  Complete: 30  |  Missing: 0
  Rules applied:   1-2s, 1-3s, 2-2s, R-4s, 4-1s, 10x

  Violations:
  -------------------------------------------------- 
    1-2s  : points 6, 15
    4-1s  : points 13-19
  -> 8 violation(s): 8 warning(s), 0 out of control

1-2s hits points 6 and 15; several overlapping 4-1s windows mark points 13–19. After combining the two rule types, there are 8 unique points, all classified as warning by the current severity implementation. Rule windows are scanned continuously across L1/L2/L3 boundaries; plot facets do not change these positions.

Code
plot(qc_lot_fit)

10.2.3 Two-Level Youden Plot

Each row contains two QC levels from the same run:

Code
youden_data <- read_csv(
  "./data/010-质量控制/qc-youden.csv",
  show_col_types = FALSE
)
youden_data <- as.data.frame(youden_data)
head(youden_data)
  run   low   high
1   1 77.86 209.36
2   2 82.19 207.10
3   3 75.50 189.00
4   4 80.40 195.36
5   5 80.64 198.23
6   6 78.22 200.14
Code
youden_fit <- youden_plot(
  youden_data,
  sample1 = "low",
  sample2 = "high",
  mean1 = 80,
  mean2 = 200,
  sd1 = 2,
  sd2 = 5
)
print(youden_fit)

Youden Plot
  Sample 1 (low):
    Mean:   80.0000
    SD:     2.0000
  Sample 2 (high):
    Mean:   200.0000
    SD:     5.0000
  Correlation: 0.5428
  N (total):   30  |  Complete: 30  |  Missing: 0
  Points outside +/-2SD: 3  (rows: 3, 17, 19)

The Pearson correlation in this example is 0.5428; original rows 3, 17, and 19 lie outside the \(±2s\) rectangle. Correlation does not determine whether a process is out of control, and an outside-rectangle flag is only a localization aid. Use each run’s two standardized deviations together with single-level control charts, instrument events, and QC records to distinguish a shared systematic shift from a problem affecting one level alone.

Code
plot(youden_fit)