Clinical submissions require a systematic approach to identifying,
scoring, and mitigating risks. The r4subrisk package
implements an FMEA (Failure Mode and Effects Analysis) based risk
framework aligned with ICH Q9 quality risk management principles.
Default configuration
risk_config_default() defines the RPN (Risk Priority
Number) band thresholds and scoring scales:
cfg <- risk_config_default()
cfg$rpn_bands
#> $critical
#> [1] 80 125
#>
#> $high
#> [1] 40 79
#>
#> $medium
#> [1] 15 39
#>
#> $low
#> [1] 1 14
cfg$required_origins
#> NULLCreating a risk register
A risk register is built from a data frame with at minimum
risk_id, description,
probability, impact, and
detectability — each scored 1–5. The RPN is computed as
probability * impact * detectability.
risks <- data.frame(
risk_id = paste0("RISK-00", 1:5),
description = c(
"Missing required variables in ADSL",
"Incomplete derivation documentation for ADTTE",
"Independent QC not completed for ADRS",
"Define-XML missing codelist references",
"ADRG not included in submission package"
),
category = c("data_quality", "traceability", "process",
"documentation", "regulatory"),
probability = c(3L, 4L, 2L, 2L, 1L),
impact = c(4L, 5L, 5L, 3L, 5L),
detectability = c(2L, 3L, 4L, 2L, 1L),
owner = c("Data Mgmt", "Stats", "Stats", "Programming", "Regulatory"),
stringsAsFactors = FALSE
)
rr <- create_risk_register(risks)
print(rr)
#> ℹ Risk Register: 5 risk(s), 5 open
#> ℹ Critical: 0, High: 2, Mean RPN: 28.2
#> # A tibble: 5 × 11
#> risk_id description category probability impact detectability owner mitigation
#> * <chr> <chr> <chr> <dbl> <dbl> <dbl> <chr> <chr>
#> 1 RISK-0… Missing re… data_qu… 3 4 2 Data… NA
#> 2 RISK-0… Incomplete… traceab… 4 5 3 Stats NA
#> 3 RISK-0… Independen… process 2 5 4 Stats NA
#> 4 RISK-0… Define-XML… documen… 2 3 2 Prog… NA
#> 5 RISK-0… ADRG not i… regulat… 1 5 1 Regu… NA
#> # ℹ 3 more variables: status <chr>, rpn <dbl>, risk_level <chr>Computing risk scores
compute_risk_scores() returns aggregate metrics
including overall risk score (normalized 0-1), mean RPN, and
per-category breakdown:
rs <- compute_risk_scores(rr)
rs$overall_risk_score
#> [1] 0.2256
rs$mean_rpn
#> [1] 28.2
rs$risk_distribution
#> # A tibble: 3 × 2
#> risk_level n
#> <chr> <int>
#> 1 high 2
#> 2 low 2
#> 3 medium 1
rs$category_summary
#> # A tibble: 5 × 3
#> category n mean_rpn
#> <chr> <int> <dbl>
#> 1 data_quality 1 24
#> 2 documentation 1 12
#> 3 process 1 40
#> 4 regulatory 1 5
#> 5 traceability 1 60Applying mitigations
apply_mitigations() updates the register with
post-control scores to show residual risk after mitigations are
applied:
controls <- data.frame(
risk_id = c("RISK-001", "RISK-002", "RISK-003"),
mitigation = c(
"Automated variable check implemented in CI pipeline",
"Derivation review added to QC checklist",
"QC sign-off required before dataset lock"
),
probability_residual = c(1L, 2L, 1L),
impact_residual = c(4L, 5L, 5L),
detectability_residual = c(1L, 1L, 1L),
stringsAsFactors = FALSE
)
rr_mitigated <- apply_mitigations(rr, controls)Comparing registers
compare_risk_registers() shows the RPN change before and
after mitigations:
comparison <- compare_risk_registers(rr, rr_mitigated)
comparison
#> $rpn_changes
#> # A tibble: 5 × 4
#> risk_id rpn_before rpn_after rpn_delta
#> <chr> <dbl> <dbl> <dbl>
#> 1 RISK-001 24 24 0
#> 2 RISK-002 60 60 0
#> 3 RISK-003 40 40 0
#> 4 RISK-004 12 12 0
#> 5 RISK-005 5 5 0
#>
#> $new_risks
#> character(0)
#>
#> $resolved_risks
#> character(0)
#>
#> $level_transitions
#> # A tibble: 5 × 4
#> risk_id level_before level_after changed
#> <chr> <chr> <chr> <lgl>
#> 1 RISK-001 medium medium FALSE
#> 2 RISK-002 high high FALSE
#> 3 RISK-003 high high FALSE
#> 4 RISK-004 low low FALSE
#> 5 RISK-005 low low FALSE
#>
#> $delta_mean_rpn
#> [1] 0Integration with r4subcore evidence
evidence_to_risks() converts an evidence table with
indicator_domain == "risk" into a risk register format,
allowing automation of risk identification from evidence outputs:
# Requires r4subcore
ctx <- r4subcore::r4sub_run_context(study_id = "STUDY01")
ev <- r4subcore::as_evidence(
data.frame(
asset_type = "program", asset_id = "prod_adrs.R",
source_name = "r4subrisk", source_version = "0.1.0",
indicator_id = "R-001", indicator_name = "Program Validation",
indicator_domain = "risk", severity = "high",
result = "fail", metric_value = 0, metric_unit = "score",
message = "DVP not submitted", location = "prod_adrs.R",
evidence_payload = "{}", stringsAsFactors = FALSE
), ctx = ctx
)
risk_df <- evidence_to_risks(ev)
rr_from_evidence <- create_risk_register(risk_df)