---
title: "Reference: Analytical Method Validation"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Reference: Analytical Method Validation}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 5
)
```

```{r setup, message = FALSE}
library(measure)
library(dplyr)
library(ggplot2)
```

## Overview

The `measure` package provides a comprehensive suite of functions for analytical method validation. These functions are designed to be compatible with regulatory frameworks including:
- **ICH Q2(R2)**: Validation of Analytical Procedures
- **ISO/IEC 17025**: General requirements for testing and calibration laboratories
- **USP <1225>**: Validation of Compendial Procedures
- **ICH M10**: Bioanalytical Method Validation (for applicable workflows)

This vignette demonstrates key validation workflows including calibration, precision, accuracy, uncertainty, and quality control.

## Calibration Curves

### Fitting Calibration Curves

The `measure_calibration_fit()` function fits weighted or unweighted calibration curves with comprehensive diagnostics.

```{r calibration-fit}
# Create calibration data
set.seed(42)
cal_data <- data.frame(
  nominal_conc = c(1, 5, 10, 25, 50, 100, 250, 500),
  response = c(1, 5, 10, 25, 50, 100, 250, 500) * 1.05 +
             rnorm(8, sd = c(0.1, 0.3, 0.5, 1, 2, 4, 10, 20))
)

# Fit with 1/x^2 weighting (common for bioanalytical methods)
cal <- measure_calibration_fit(
  cal_data,
  response ~ nominal_conc,
  weights = "1/x2"
)

print(cal)
```

### Visualizing the Calibration

```{r calibration-plot}
autoplot(cal, type = "curve")
```

### Checking Residuals

```{r calibration-residuals}
autoplot(cal, type = "residuals")
```

### Predicting Unknown Concentrations

```{r calibration-predict}
unknowns <- data.frame(
  sample_id = c("Sample_1", "Sample_2", "Sample_3"),
  response = c(52.3, 125.8, 280.5)
)

predictions <- measure_calibration_predict(
  cal,
  newdata = unknowns,
  interval = "confidence"
)

cbind(unknowns, predictions)
```

### Calibration Verification

Verify that the calibration remains valid using QC samples:

```{r calibration-verify}
qc_data <- data.frame(
  sample_id = c("QC_Low", "QC_Mid", "QC_High"),
  nominal_conc = c(3, 75, 400),
  response = c(3.1, 77.5, 395.2)
)

verification <- measure_calibration_verify(cal, qc_data)
print(verification)
```

## Limits of Detection and Quantitation

### Multiple Methods

`measure` supports multiple approaches for calculating LOD/LOQ:

```{r lod-loq}
# Blank-based approach (3σ/10σ)
blank_data <- data.frame(
  sample_type = rep("blank", 10),
  response = rnorm(10, mean = 0.5, sd = 0.08)
)

lod_result <- measure_lod(
  blank_data,
  "response",
  method = "blank_sd",
  calibration = cal
)
print(lod_result)

# Or calculate both together
lod_loq <- measure_lod_loq(
  blank_data,
  "response",
  method = "blank_sd",
  calibration = cal
)
tidy(lod_loq)
```

## Precision Studies

### Repeatability (Within-Run Precision)

```{r repeatability}
# Data from replicate measurements
repeat_data <- data.frame(
  sample_id = rep(c("Low", "Mid", "High"), each = 6),
  concentration = c(
    rnorm(6, 10, 0.5),
    rnorm(6, 100, 4),
    rnorm(6, 500, 18)
  )
)

repeatability <- measure_repeatability(
  repeat_data,
  "concentration",
  group_col = "sample_id"
)
print(repeatability)
```

### Intermediate Precision

```{r intermediate-precision}
# Data from multiple days
ip_data <- data.frame(
  day = rep(1:3, each = 6),
  analyst = rep(c("A", "A", "A", "B", "B", "B"), 3),
  concentration = 100 +
    rep(c(-2, 0, 2), each = 6) +  # Day effect
    rep(c(-1, 1), 9) +            # Analyst effect
    rnorm(18, sd = 3)             # Residual
)

ip_result <- measure_intermediate_precision(
  ip_data,
  "concentration",
  factors = c("day", "analyst")
)
print(ip_result)
```

### Gage R&R Analysis

For measurement system analysis:

```{r gage-rr}
# Gage R&R data
grr_data <- data.frame(
  part = rep(1:5, each = 6),
  operator = rep(rep(c("Op1", "Op2"), each = 3), 5),
  measurement = c(
    # Part 1
    10.1, 10.2, 10.0, 10.3, 10.1, 10.2,
    # Part 2
    20.2, 20.1, 20.3, 20.0, 20.2, 20.1,
    # Part 3
    15.1, 15.0, 15.2, 15.3, 15.1, 15.0,
    # Part 4
    25.0, 25.1, 24.9, 25.2, 25.0, 25.1,
    # Part 5
    30.1, 30.2, 30.0, 30.1, 30.0, 30.2
  )
)

grr_result <- measure_gage_rr(
  grr_data,
  "measurement",
  part_col = "part",
  operator_col = "operator"
)
print(grr_result)
```

## Accuracy Assessment

### Bias and Recovery

```{r accuracy}
accuracy_data <- data.frame(
  level = rep(c("Low", "Mid", "High"), each = 5),
  measured = c(
    rnorm(5, 10.2, 0.3),   # Low level, slight positive bias
    rnorm(5, 100, 2.5),    # Mid level, no bias
    rnorm(5, 498, 8)       # High level, slight negative bias
  ),
  reference = rep(c(10, 100, 500), each = 5)
)

accuracy <- measure_accuracy(
  accuracy_data,
  "measured",
  "reference",
  group_col = "level"
)
print(accuracy)
```

### Linearity Assessment

```{r linearity}
linearity_data <- data.frame(
  concentration = rep(c(10, 25, 50, 75, 100), each = 3),
  response = rep(c(10, 25, 50, 75, 100), each = 3) * 1.02 +
             rnorm(15, sd = 1.5)
)

linearity <- measure_linearity(
  linearity_data,
  "concentration",
  "response"
)
print(linearity)

# Plot with fit line
autoplot(linearity, type = "fit")
```

## Uncertainty Budgets

### ISO GUM Uncertainty

Create uncertainty budgets following the GUM (Guide to the Expression of Uncertainty in Measurement):

```{r uncertainty}
# Define uncertainty components
components <- list(
  uncertainty_component(
    name = "Repeatability",
    type = "A",
    value = 0.5,
    df = 9
  ),
  uncertainty_component(
    name = "Calibration",
    type = "B",
    value = 0.3,
    distribution = "normal"
  ),
  uncertainty_component(
    name = "Reference Standard",
    type = "B",
    value = 0.1,
    distribution = "rectangular"
  ),
  uncertainty_component(
    name = "Temperature",
    type = "B",
    value = 0.2,
    sensitivity = 0.5  # Sensitivity coefficient
  )
)

budget <- measure_uncertainty_budget(.list = components)
print(budget)
```

### Visualizing Uncertainty Contributions

```{r uncertainty-plot}
autoplot(budget)
```

## Control Charts

### Setting Up Control Limits

```{r control-limits}
# Historical QC data
qc_history <- data.frame(
  run_order = 1:30,
  qc_value = rnorm(30, mean = 100, sd = 2)
)

limits <- measure_control_limits(qc_history, "qc_value")
print(limits)
```
### Monitoring with Westgard Rules

```{r control-chart}
# New run data including potential out-of-control point
new_run <- data.frame(
  run_order = 1:20,
  qc_value = c(rnorm(19, 100, 2), 108)  # Last point is high
)

chart <- measure_control_chart(
  new_run,
  "qc_value",
  "run_order",
  limits = limits,
  rules = c("1_3s", "2_2s", "R_4s", "10x")
)
print(chart)
```

```{r control-chart-plot}
autoplot(chart)
```

## Acceptance Criteria

### Defining Criteria

```{r criteria}
# Create custom criteria
my_criteria <- measure_criteria(
  criterion("cv", "<=", 15, description = "Precision CV"),
  criterion("bias_pct", "between", c(-10, 10), description = "Bias"),
  criterion("recovery", "between", c(85, 115), description = "Recovery %")
)
print(my_criteria)
```

### Using Preset Criteria

```{r preset-criteria}
# ICH Q2 presets
ich_criteria <- criteria_ich_q2()
print(ich_criteria)

# Bioanalytical presets
bio_criteria <- criteria_bioanalytical()
print(bio_criteria)
```

### Assessing Results

```{r assess}
# Sample results to assess (single summary values per criterion)
# For example, from a method validation summary
results <- list(
  cv = 5.2,          # Overall precision CV
  bias_pct = 1.3,    # Overall bias
  recovery = 101.3   # Mean recovery
)

assessment <- measure_assess(results, my_criteria)
print(assessment)

# Check if all criteria passed
all_pass(assessment)
```

## Method Comparison

When validating a new method, you often need to compare it against a reference or existing method. The `measure` package provides several approaches for method comparison studies.

### Bland-Altman Analysis

Bland-Altman plots show the agreement between two methods by plotting differences against means:

```{r bland-altman}
# Paired measurements from two methods
comparison_data <- data.frame(
  sample_id = 1:30,
  method_A = rnorm(30, mean = 100, sd = 15),
  method_B = rnorm(30, mean = 102, sd = 16)
)

ba <- measure_bland_altman(
  comparison_data,
  method1_col = "method_A",
  method2_col = "method_B",
  regression = "linear"  # Test for proportional bias
)
print(ba)
```

```{r bland-altman-plot}
autoplot(ba)
```

### Regression Methods

For method comparison regression, use Deming or Passing-Bablok regression which account for error in both methods:

```{r deming-regression}
# Method comparison with known measurement error
deming_data <- data.frame(
  reference = c(5, 10, 25, 50, 100, 200, 400),
  test_method = c(5.2, 10.3, 25.8, 51.2, 101.5, 203.1, 408.2)
)

deming <- measure_deming_regression(
  deming_data,
  method1_col = "reference",
  method2_col = "test_method",
  bootstrap = TRUE,
  bootstrap_n = 500
)
print(deming)

# Check if methods are equivalent
glance(deming)
```

For Passing-Bablok regression (non-parametric), install the `mcr` package:

```{r passing-bablok, eval = FALSE}
# Requires: install.packages("mcr")
pb <- measure_passing_bablok(
  deming_data,
  method1_col = "reference",
  method2_col = "test_method"
)
print(pb)
```

### Proficiency Testing

Evaluate laboratory performance in proficiency testing programs:

```{r proficiency-score}
# PT results from multiple labs
pt_data <- data.frame(
  lab_id = paste0("Lab_", 1:10),
  measured = c(99.2, 100.5, 98.8, 101.2, 97.5, 100.1, 99.8, 102.3, 100.6, 94.0),
  assigned = rep(100, 10),
  uncertainty = c(1.5, 2.0, 1.8, 1.6, 2.2, 1.9, 1.7, 2.1, 1.5, 2.0)
)

# z-scores with known sigma
z_scores <- measure_proficiency_score(
  pt_data,
  measured_col = "measured",
  reference_col = "assigned",
  score_type = "z_score",
  sigma = 2.5
)
print(z_scores)
```

```{r proficiency-plot}
autoplot(z_scores)
```

## Matrix Effects

Matrix effects (ion suppression/enhancement) must be evaluated in LC-MS/MS and similar methods.

### Evaluating Matrix Effects

```{r matrix-effect}
# Post-extraction spike experiment
me_data <- data.frame(
  sample_type = rep(c("matrix", "neat"), each = 6),
  matrix_lot = rep(c("Lot1", "Lot2", "Lot3"), 4),
  concentration = rep(c("low", "high"), each = 3, times = 2),
  response = c(
    # Matrix samples (some suppression)
    9200, 9500, 8900, 47500, 48200, 46800,
    # Neat samples
    10000, 10000, 10000, 50000, 50000, 50000
  )
)

me <- measure_matrix_effect(
  me_data,
  response_col = "response",
  sample_type_col = "sample_type",
  matrix_level = "matrix",
  neat_level = "neat",
  concentration_col = "concentration"
)
print(me)
```

```{r matrix-effect-plot}
autoplot(me, type = "bar")
```

### Standard Addition Correction

When matrix effects vary between samples, standard addition provides sample-specific correction:

```{r standard-addition, eval = FALSE}
library(recipes)

# Standard addition data
sa_data <- data.frame(
  sample_id = rep(c("Sample1", "Sample2"), each = 4),
  addition = rep(c(0, 10, 20, 30), 2),
  response = c(
    150, 250, 350, 450,  # Sample 1
    250, 350, 450, 550   # Sample 2
  )
)

rec <- recipe(~ ., data = sa_data) |>
  step_measure_standard_addition(
    response,
    addition_col = "addition",
    sample_id_col = "sample_id"
  ) |>
  prep()

# Original concentrations calculated via extrapolation
bake(rec, new_data = NULL)
```

## Sample Preparation QC

Recipe steps for quality control during sample preparation.

### Dilution Factor Correction

Back-calculate concentrations for diluted samples:

```{r dilution-correction}
library(recipes)

dilution_data <- data.frame(
  sample_id = paste0("S", 1:5),
  dilution_factor = c(1, 2, 5, 10, 1),
  analyte = c(50, 45, 42, 48, 51)  # Measured after dilution
)

rec <- recipe(~ ., data = dilution_data) |>
  update_role(sample_id, new_role = "id") |>
  step_measure_dilution_correct(
    analyte,
    dilution_col = "dilution_factor",
    operation = "multiply"
  ) |>
  prep()

# Back-calculated original concentrations
bake(rec, new_data = NULL)
```

### Surrogate Recovery

Monitor extraction efficiency with surrogate standards:

```{r surrogate-recovery}
qc_data <- data.frame(
  sample_id = paste0("QC", 1:6),
  surrogate = c(95, 105, 88, 112, 75, 132)  # Expected = 100
)

rec <- recipe(~ ., data = qc_data) |>
  update_role(sample_id, new_role = "id") |>
  step_measure_surrogate_recovery(
    surrogate,
    expected_value = 100,
    action = "flag",
    min_recovery = 80,
    max_recovery = 120
  ) |>
  prep()

# Flag samples outside recovery limits
bake(rec, new_data = NULL)
```

## Drift Correction

### Detecting Drift

```{r detect-drift}
# Data with drift
drift_data <- data.frame(
  sample_type = rep("qc", 20),
  run_order = 1:20,
  feature1 = 100 + (1:20) * 0.8 + rnorm(20, sd = 2),  # Has drift

  feature2 = 100 + rnorm(20, sd = 2)                   # No drift
)

drift_result <- measure_detect_drift(
  drift_data,
  features = c("feature1", "feature2"),
  qc_type = "qc"
)
print(drift_result)
```

### Correcting Drift

```{r drift-correction, eval = FALSE}
library(recipes)

# Using QC-LOESS correction in a recipe
rec <- recipe(~ ., data = drift_data) |>
  step_measure_drift_qc_loess(
    feature1, feature2,
    qc_type = "qc"
  ) |>
  prep()

corrected <- bake(rec, new_data = NULL)
```

## Validation Reports

Once you've completed your validation studies, you can compile all results into a reproducible validation report using `measure_validation_report()`. The package provides templates following regulatory frameworks like ICH Q2(R2) and USP <1225>.
### Creating a Validation Report

```{r validation-report}
# Gather validation results (using objects from above)
report <- measure_validation_report(
  # Metadata
  title = "HPLC-UV Method Validation Report",
  method_name = "Compound X Assay",
  method_description = "Reversed-phase HPLC with UV detection at 254 nm",
  analyst = "J. Smith",
  reviewer = "A. Jones",
  lab = "Analytical Development",
  instrument = "Agilent 1260 Infinity II",


  # Validation sections (results from earlier in this vignette)
  calibration = cal,
  lod_loq = lod_loq,
  accuracy = accuracy,
  precision = list(repeatability = repeatability, intermediate = ip_result),
  linearity = linearity,
  range = list(lower = 1, upper = 500, units = "ng/mL"),
  uncertainty = budget,

  # Text sections
  specificity = "No interfering peaks observed at the analyte retention time when analyzing blank matrix samples.",
  robustness = list(
    factors = c("Flow rate (±0.1 mL/min)", "Column temperature (±5°C)", "Mobile phase pH (±0.2)"),
    conclusion = "Method showed acceptable robustness within tested parameter ranges."
  ),

  # Conclusions
  conclusions = list(
    summary = "The analytical method meets all acceptance criteria for precision, accuracy, and linearity.",
    recommendations = c(
      "Method is suitable for intended use",
      "Revalidate if significant changes are made to instrumentation or reagents"
    )
  ),

  # References
  references = c(
    "ICH Q2(R2) Validation of Analytical Procedures (2023)",
    "USP <1225> Validation of Compendial Procedures"
  )
)

print(report)
```

### Inspecting Report Sections

```{r report-sections}
# Check which sections are included
summary(report)

# Access specific sections
has_validation_section(report, "calibration")
has_validation_section(report, "stability")  # Not included

# Get section data
get_validation_section(report, "range")
```

### Adding Custom Sections

You can add additional sections after report creation:

```{r add-section}
# Add a stability section later
report <- add_validation_section(
  report,
  "stability",
  list(
    description = "Short-term stability at room temperature",
    results = data.frame(
      timepoint = c("0h", "4h", "8h", "24h"),
      recovery_pct = c(100, 99.5, 98.8, 97.2)
    ),
    conclusion = "Sample is stable for 24 hours at room temperature."
  )
)

has_validation_section(report, "stability")
```

### Tidy Output

Extract all results as a tidy tibble for further analysis:

```{r tidy-report}
tidy(report)
```

### Rendering Reports

To render a validation report to HTML or PDF, use `render_validation_report()`. Two templates are provided:

- **ICH Q2(R2)**: Organized by validation characteristics (specificity, linearity, range, accuracy, precision, etc.)
- **USP <1225>**: Organized by procedure category (I, II, III, IV)

```{r render-report, eval = FALSE}
# Render to HTML using ICH Q2 template (default)
render_validation_report(
  report,
  output_file = "validation_report.html",
  template = "ich_q2"
)

# Render to PDF using USP <1225> template
render_validation_report(
  report,
  output_file = "validation_report.pdf",
  output_format = "pdf",
  template = "usp_1225"
)

# Use a custom Quarto template
render_validation_report(
  report,
  output_file = "custom_report.html",
  template_path = "path/to/custom_template.qmd"
)
```

The rendered report includes:

- **Header**: Method name, date, analyst, reviewer, lab, instrument
- **Table of Contents**: Auto-generated from sections
- **Validation Sections**: Each with formatted tables and plots
- **Provenance**: R version, package versions, timestamp for reproducibility

## Summary

The `measure` package provides a complete toolkit for analytical method validation:

| Category | Key Functions |
|----------|---------------|
| **Calibration** | `measure_calibration_fit()`, `measure_calibration_predict()`, `measure_calibration_verify()` |
| **LOD/LOQ** | `measure_lod()`, `measure_loq()`, `measure_lod_loq()` |
| **Precision** | `measure_repeatability()`, `measure_intermediate_precision()`, `measure_gage_rr()` |
| **Accuracy** | `measure_accuracy()`, `measure_linearity()`, `measure_carryover()` |
| **Method Comparison** | `measure_bland_altman()`, `measure_deming_regression()`, `measure_passing_bablok()`, `measure_proficiency_score()` |
| **Matrix Effects** | `measure_matrix_effect()`, `step_measure_standard_addition()` |
| **Sample Prep QC** | `step_measure_dilution_correct()`, `step_measure_surrogate_recovery()` |
| **Uncertainty** | `measure_uncertainty_budget()`, `measure_uncertainty()` |
| **Control Charts** | `measure_control_limits()`, `measure_control_chart()` |
| **Criteria** | `measure_criteria()`, `measure_assess()`, `criteria_ich_q2()`, `criteria_bland_altman()`, `criteria_matrix_effects()` |
| **Drift** | `measure_detect_drift()`, `step_measure_drift_qc_loess()` |
| **Validation Reports** | `measure_validation_report()`, `render_validation_report()` |

All functions follow a consistent design philosophy:
- **Tidy outputs**: Results are tibbles with `tidy()`, `glance()`, and `autoplot()` methods
- **Transparent diagnostics**: No hidden decisions; all parameters and flags are visible
- **Regulatory compatibility**: Designed with ICH, ISO, and FDA guidelines in mind
- **Provenance tracking**: Audit trails for outlier handling and data modifications

For more details on any function, see the package documentation with `?function_name`.

## See Also

- [Getting Started](../articles/tutorial-getting-started.html) - Introduction to measure workflows
- [Preprocessing Reference](../articles/reference-preprocessing.html) - Guide to preprocessing techniques
